From 0389f0d3002c037898743836f15fc81f4736e846 Mon Sep 17 00:00:00 2001 From: Olivier Gorzalka Date: Wed, 30 Sep 2026 15:50:51 +0200 Subject: [PATCH 1/2] fix: pollora:install completes without a terminal Missing options get working defaults when the command cannot prompt; a generated admin password is shown once. --- CHANGELOG.md | 3 + .../Installation/DTO/InstallationConfig.php | 38 +++++++++++ .../Commands/LaunchPadInstallCommand.php | 28 +++++--- .../Commands/LaunchPadInstallCommandTest.php | 67 +++++++++++++++++-- 4 files changed, 123 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b11678d7..f54ced96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/Pollora/framework/compare/v13.34.0...develop) +### Fixed +- `pollora:install` without a terminal — `--no-interaction`, CI, or `pollora new` driving it — stopped on "Site title is required" unless every option was passed: the prompts it could not show were still required. It now fills what is missing with a working local site — the project directory as title, `admin` at `admin@`, a generated password shown once (also with `--install`), `en_US`, not indexed — and keeps every option it is given + ## [v13.34.0](https://github.com/Pollora/framework/compare/v13.34.0-beta.2...v13.34.0) - 2026-09-30 The first stable release since v13.4.4, on Laravel 13.34. It gathers every change from v13.32.0-beta to v13.34.0-beta.2 below; coming from 13.4, read those entries, or run Nectar's `upgrade-pollora-v13-32` prompt ([full comparison](https://github.com/Pollora/framework/compare/v13.4.4...v13.34.0)). diff --git a/src/Services/WordPress/Installation/DTO/InstallationConfig.php b/src/Services/WordPress/Installation/DTO/InstallationConfig.php index f88a7bfe..e0a39f95 100755 --- a/src/Services/WordPress/Installation/DTO/InstallationConfig.php +++ b/src/Services/WordPress/Installation/DTO/InstallationConfig.php @@ -4,6 +4,7 @@ namespace Pollora\Services\WordPress\Installation\DTO; +use Illuminate\Support\Str; use Pollora\Services\WordPress\Installation\LanguageService; use function Laravel\Prompts\confirm; @@ -88,4 +89,41 @@ public static function fromPrompts( ), ); } + + /** + * Create configuration for an install that cannot prompt. + * + * Without a terminal — `--no-interaction`, CI, or `pollora new` driving the + * command — prompts cannot be answered, and a required one used to abort the + * install on "Site title is required". Every option given is kept; the rest + * gets a value that makes a working local site: the project directory as + * title, `admin` on the site's own domain, a generated password, `en_US`, + * not indexed. + * + * @param string $projectName Directory name of the project, for the title + * @param string|null $siteUrl The application URL, for the admin email domain + */ + public static function withDefaults( + string $projectName, + ?string $siteUrl, + ?string $title = null, + ?string $description = null, + ?string $adminUser = null, + ?string $adminEmail = null, + ?string $adminPassword = null, + ?string $locale = null, + ?bool $isPublic = null, + ): self { + $host = is_string($siteUrl) ? parse_url($siteUrl, PHP_URL_HOST) : null; + + return new self( + title: $title ?? Str::headline($projectName), + description: $description ?? '', + adminUser: $adminUser ?? 'admin', + adminEmail: $adminEmail ?? 'admin@'.(is_string($host) && str_contains($host, '.') ? $host : 'example.com'), + adminPassword: $adminPassword ?? Str::password(20, symbols: false), + locale: $locale ?? 'en_US', + isPublic: $isPublic ?? false, + ); + } } diff --git a/src/WordPress/Commands/LaunchPadInstallCommand.php b/src/WordPress/Commands/LaunchPadInstallCommand.php index 05c6dc59..3c7d3c6b 100644 --- a/src/WordPress/Commands/LaunchPadInstallCommand.php +++ b/src/WordPress/Commands/LaunchPadInstallCommand.php @@ -15,6 +15,7 @@ use function Laravel\Prompts\error; use function Laravel\Prompts\info; +use function Laravel\Prompts\warning; #[Description('Install and configure WordPress')] #[Signature('pollora:install @@ -83,18 +84,27 @@ private function installWordPress(bool $silent = false): void info('Starting WordPress installation...'); } - $config = InstallationConfig::fromPrompts( - title: $this->option('title'), - description: $this->option('description'), - adminUser: $this->option('admin-user'), - adminEmail: $this->option('admin-email'), - adminPassword: $this->option('admin-password'), - locale: $this->option('locale'), - isPublic: $this->option('public') !== null ? filter_var($this->option('public'), FILTER_VALIDATE_BOOLEAN) : null, - ); + $options = [ + 'title' => $this->option('title'), + 'description' => $this->option('description'), + 'adminUser' => $this->option('admin-user'), + 'adminEmail' => $this->option('admin-email'), + 'adminPassword' => $this->option('admin-password'), + 'locale' => $this->option('locale'), + 'isPublic' => $this->option('public') !== null ? filter_var($this->option('public'), FILTER_VALIDATE_BOOLEAN) : null, + ]; + + $config = $this->input->isInteractive() + ? InstallationConfig::fromPrompts(...$options) + : InstallationConfig::withDefaults(basename(base_path()), config('app.url'), ...$options); $this->installationService->install($config); + if ($options['adminPassword'] === null && ! $this->input->isInteractive()) { + // Generated here and stored hashed: this is the only time it is shown. + warning(sprintf('Admin account: %s / %s — generated, shown only once.', $config->adminUser, $config->adminPassword)); + } + $this->runMigrations(); $this->installTheme(); diff --git a/tests/Unit/WordPress/Commands/LaunchPadInstallCommandTest.php b/tests/Unit/WordPress/Commands/LaunchPadInstallCommandTest.php index 0e9e039e..772d84e5 100644 --- a/tests/Unit/WordPress/Commands/LaunchPadInstallCommandTest.php +++ b/tests/Unit/WordPress/Commands/LaunchPadInstallCommandTest.php @@ -2,12 +2,14 @@ declare(strict_types=1); +use Illuminate\Config\Repository; use Illuminate\Console\Application; use Illuminate\Console\Attributes\Signature; use Illuminate\Console\Command; use Illuminate\Container\Container; use Illuminate\Events\Dispatcher; use Pollora\Services\WordPress\Installation\DatabaseService; +use Pollora\Services\WordPress\Installation\DTO\InstallationConfig; use Pollora\Services\WordPress\Installation\InstallationService; use Pollora\WordPress\Commands\LaunchPadInstallCommand; use Symfony\Component\Console\Input\ArrayInput; @@ -18,13 +20,16 @@ * migrate are replaced by commands recording the input they receive. * * @param array $parameters - * @return array{exit: int, theme: array|null, migrate: array|null} + * @return array{exit: int, theme: array|null, migrate: array|null, config: InstallationConfig|null, output: string} */ function runInstallCommand(array $parameters, bool $interactive, int $migrateExit = 0): array { + $config = null; $installation = Mockery::mock(InstallationService::class); $installation->shouldReceive('isInstalled')->andReturn(false); - $installation->shouldReceive('install')->once(); + $installation->shouldReceive('install')->once()->andReturnUsing(function (InstallationConfig $given) use (&$config): void { + $config = $given; + }); $database = Mockery::mock(DatabaseService::class); $database->shouldReceive('isConfigured')->andReturn(true); @@ -74,7 +79,13 @@ public function isLocal(): bool { return false; } + + public function basePath(string $path = ''): string + { + return '/srv/acme-site'.($path !== '' ? '/'.$path : ''); + } }; + $container->instance('config', new Repository(['app' => ['url' => 'https://acme-site.ddev.site']])); // handleError() resolves app() to check the environment $previousContainer = Container::getInstance(); Container::setInstance($container); @@ -90,12 +101,13 @@ public function isLocal(): bool $input->setInteractive($interactive); try { - $exit = $application->find('pollora:install')->run($input, new BufferedOutput); + $output = new BufferedOutput; + $exit = $application->find('pollora:install')->run($input, $output); } finally { Container::setInstance($previousContainer); } - return ['exit' => $exit, 'theme' => $recorder->received, 'migrate' => $migrate->received]; + return ['exit' => $exit, 'theme' => $recorder->received, 'migrate' => $migrate->received, 'config' => $config, 'output' => $output->fetch()]; } describe('pollora:install theme generation', function (): void { @@ -163,3 +175,50 @@ public function isLocal(): bool ->and($result['theme'])->toBeNull(); }); }); + +describe('pollora:install without prompts', function (): void { + beforeEach(function (): void { + Brain\Monkey\Functions\when('admin_url')->justReturn('https://acme-site.ddev.site/wp-admin/'); + }); + + it('installs with working defaults instead of failing on the first required prompt', function (): void { + // `pollora new --ddev --no-interaction` ran it with no option and no + // terminal, and the install stopped on "Site title is required". + $result = runInstallCommand(['--install' => true], interactive: false); + + expect($result['exit'])->toBe(0) + ->and($result['config'])->toBeInstanceOf(InstallationConfig::class) + ->and($result['config']->title)->toBe('Acme Site') + ->and($result['config']->adminUser)->toBe('admin') + ->and($result['config']->adminEmail)->toBe('admin@acme-site.ddev.site') + ->and(strlen($result['config']->adminPassword))->toBe(20) + ->and($result['config']->locale)->toBe('en_US') + ->and($result['config']->isPublic)->toBeFalse(); + }); + + it('shows a generated password once, even in --install mode', function (): void { + $result = runInstallCommand(['--install' => true], interactive: false); + + expect($result['output'])->toContain($result['config']->adminPassword); + }); + + it('keeps every option it is given', function (): void { + $result = runInstallCommand([ + '--install' => true, + '--title' => 'Pollora', + '--admin-user' => 'olivier', + '--admin-email' => 'dev@example.org', + '--admin-password' => 'secret123', + '--locale' => 'fr_FR', + '--public' => 'true', + ], interactive: false); + + expect($result['config']->title)->toBe('Pollora') + ->and($result['config']->adminUser)->toBe('olivier') + ->and($result['config']->adminEmail)->toBe('dev@example.org') + ->and($result['config']->adminPassword)->toBe('secret123') + ->and($result['config']->locale)->toBe('fr_FR') + ->and($result['config']->isPublic)->toBeTrue() + ->and($result['output'])->not->toContain('secret123'); + }); +}); From 93e34f165bd1524f6d08660f40ab82e5bdff3942 Mon Sep 17 00:00:00 2001 From: Olivier Gorzalka Date: Wed, 30 Sep 2026 16:01:17 +0200 Subject: [PATCH 2/2] fix: name the site after the APP_URL host, not /var/www/html --- CHANGELOG.md | 2 +- .../Installation/DTO/InstallationConfig.php | 4 ++-- .../Commands/LaunchPadInstallCommand.php | 20 ++++++++++++++++++- .../Commands/LaunchPadInstallCommandTest.php | 3 ++- 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f54ced96..0d690454 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/Pollora/framework/compare/v13.34.0...develop) ### Fixed -- `pollora:install` without a terminal — `--no-interaction`, CI, or `pollora new` driving it — stopped on "Site title is required" unless every option was passed: the prompts it could not show were still required. It now fills what is missing with a working local site — the project directory as title, `admin` at `admin@`, a generated password shown once (also with `--install`), `en_US`, not indexed — and keeps every option it is given +- `pollora:install` without a terminal — `--no-interaction`, CI, or `pollora new` driving it — stopped on "Site title is required" unless every option was passed: the prompts it could not show were still required. It now fills what is missing with a working local site — the project name as title (the first label of the `APP_URL` host: under DDEV the directory is always `html`), `admin` at `admin@`, a generated password shown once (also with `--install`), `en_US`, not indexed — and keeps every option it is given ## [v13.34.0](https://github.com/Pollora/framework/compare/v13.34.0-beta.2...v13.34.0) - 2026-09-30 diff --git a/src/Services/WordPress/Installation/DTO/InstallationConfig.php b/src/Services/WordPress/Installation/DTO/InstallationConfig.php index e0a39f95..5a2c3270 100755 --- a/src/Services/WordPress/Installation/DTO/InstallationConfig.php +++ b/src/Services/WordPress/Installation/DTO/InstallationConfig.php @@ -96,11 +96,11 @@ public static function fromPrompts( * Without a terminal — `--no-interaction`, CI, or `pollora new` driving the * command — prompts cannot be answered, and a required one used to abort the * install on "Site title is required". Every option given is kept; the rest - * gets a value that makes a working local site: the project directory as + * gets a value that makes a working local site: the project's name as * title, `admin` on the site's own domain, a generated password, `en_US`, * not indexed. * - * @param string $projectName Directory name of the project, for the title + * @param string $projectName The project's name, for the title * @param string|null $siteUrl The application URL, for the admin email domain */ public static function withDefaults( diff --git a/src/WordPress/Commands/LaunchPadInstallCommand.php b/src/WordPress/Commands/LaunchPadInstallCommand.php index 3c7d3c6b..609d1443 100644 --- a/src/WordPress/Commands/LaunchPadInstallCommand.php +++ b/src/WordPress/Commands/LaunchPadInstallCommand.php @@ -96,7 +96,7 @@ private function installWordPress(bool $silent = false): void $config = $this->input->isInteractive() ? InstallationConfig::fromPrompts(...$options) - : InstallationConfig::withDefaults(basename(base_path()), config('app.url'), ...$options); + : InstallationConfig::withDefaults($this->projectName(), config('app.url'), ...$options); $this->installationService->install($config); @@ -112,6 +112,24 @@ private function installWordPress(bool $silent = false): void $this->displaySuccessMessage(); } + /** + * The project's name, for a site title nobody was asked for. + * + * The first label of the application's host (`acme.ddev.site` → `acme`): + * under DDEV or Docker the project always lives in `/var/www/html`, whose + * directory name says nothing. The directory name only without a real host. + */ + private function projectName(): string + { + $host = parse_url((string) config('app.url'), PHP_URL_HOST); + + if (is_string($host) && str_contains($host, '.')) { + return explode('.', $host)[0]; + } + + return basename(base_path()); + } + private function installTheme(): void { $arguments = []; diff --git a/tests/Unit/WordPress/Commands/LaunchPadInstallCommandTest.php b/tests/Unit/WordPress/Commands/LaunchPadInstallCommandTest.php index 772d84e5..0c79a7da 100644 --- a/tests/Unit/WordPress/Commands/LaunchPadInstallCommandTest.php +++ b/tests/Unit/WordPress/Commands/LaunchPadInstallCommandTest.php @@ -82,7 +82,8 @@ public function isLocal(): bool public function basePath(string $path = ''): string { - return '/srv/acme-site'.($path !== '' ? '/'.$path : ''); + // DDEV and Docker keep every project in /var/www/html + return '/var/www/html'.($path !== '' ? '/'.$path : ''); } }; $container->instance('config', new Repository(['app' => ['url' => 'https://acme-site.ddev.site']]));