From bddd04d74097604ac6601d4fd53ea4a4d0c15b64 Mon Sep 17 00:00:00 2001 From: roble Date: Sat, 5 Sep 2026 17:38:55 +0100 Subject: [PATCH] feat: add docker:publish command for publishing Docker files Adds `saucebase docker:publish` so Docker files can be published (or refreshed) in an existing app without running the full install flow. Existing files prompt before being overwritten; --force skips the prompt and --ssl=no publishes the plain-HTTP nginx config. DockerEnvironment::publishStubs() now delegates to the command so there is a single copy path. It calls it non-interactively and does not pass install's --force through, so re-running install still keeps existing Docker files. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 3 +- src/Console/Application.php | 2 + src/Console/Commands/PublishDockerCommand.php | 60 ++++++++++++++++ src/Environments/DockerEnvironment.php | 36 ++-------- tests/Feature/PublishDockerCommandTest.php | 72 +++++++++++++++++++ 5 files changed, 141 insertions(+), 32 deletions(-) create mode 100644 src/Console/Commands/PublishDockerCommand.php create mode 100644 tests/Feature/PublishDockerCommandTest.php diff --git a/CLAUDE.md b/CLAUDE.md index cfe799b..bf0b5dc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,6 +9,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - `saucebase new ` — creates a new project (via `laravel/installer`) and runs the full install flow against it. - `saucebase install` — runs the install flow against an existing Saucebase app in the current directory (used internally by `new`, and available standalone). - `saucebase stack ` — selects/switches the frontend framework in an app directory. +- `saucebase docker:publish` — publishes the Docker stubs into an app directory (`--force` overwrites, `--ssl=no` picks the plain-HTTP nginx config). It is **not** a Laravel package — there is no service provider and no package discovery. It runs standalone via a minimal `Illuminate\Console\Application` (see `src/Console/Application.php`). Local PHP + Composer are the only universal prerequisites (same as `laravel/installer` itself). Docker stubs (`docker-compose.yml`, `Dockerfile`, `nginx.conf`, `php.ini`, `xdebug.ini`) live in `stubs/docker/` and are copied directly into the target app by `DockerEnvironment::publishStubs()` (no `vendor:publish`). @@ -58,7 +59,7 @@ composer install **Docker flow** (`DockerEnvironment::boot()`): 1. `promptForSsl()` — `--ssl=yes|no` if given, else `--force` ⇒ on, else prompt (requires mkcert) 2. SSL gate: requested but no `mkcert` → FAILURE with install hint -3. `publishStubs()` — **copies `stubs/docker/*` directly** into the target app (skips files that already exist); if SSL off, overwrites `docker/nginx.conf` with `nginx-no-ssl.conf` +3. `publishStubs()` — delegates to the `docker:publish` command (non-interactively, so existing files are kept); if SSL off it publishes `nginx-no-ssl.conf` as `docker/nginx.conf` 4. `generateSsl()` — mkcert for `*.localhost` (no-op if disabled or certs exist) 5. `ensureEnvFile()` → `setDockerEnvDefaults()` → `applyDockerEnvDefaults()`: `DB_CONNECTION=mysql`, MySQL creds, `MAIL_MAILER=smtp`, `APP_URL=https://localhost` (or `http://` if SSL off) 6. `startDocker()` — `docker compose restart` + `up -d --wait --build` (30 min timeout, streaming), cwd = target diff --git a/src/Console/Application.php b/src/Console/Application.php index 9597216..a6c4cf4 100644 --- a/src/Console/Application.php +++ b/src/Console/Application.php @@ -9,6 +9,7 @@ use Illuminate\Support\Facades\Facade; use Saucebase\Installer\Console\Commands\InstallCommand; use Saucebase\Installer\Console\Commands\NewCommand; +use Saucebase\Installer\Console\Commands\PublishDockerCommand; use Saucebase\Installer\Console\Commands\StackCommand; class Application @@ -27,6 +28,7 @@ public static function make(): ConsoleApplication NewCommand::class, InstallCommand::class, StackCommand::class, + PublishDockerCommand::class, ]); return $console; diff --git a/src/Console/Commands/PublishDockerCommand.php b/src/Console/Commands/PublishDockerCommand.php new file mode 100644 index 0000000..8a14767 --- /dev/null +++ b/src/Console/Commands/PublishDockerCommand.php @@ -0,0 +1,60 @@ +option('path') ?: getcwd(); + $force = (bool) $this->option('force'); + + foreach (self::FILES as $file) { + $destination = $base.'/'.$file; + + if (file_exists($destination) && ! $force && ! $this->confirm("{$file} already exists. Overwrite it?", false)) { + $this->line(" Skipped {$file}"); + + continue; + } + + @mkdir(dirname($destination), 0755, true); + + if (! copy($stubs.'/'.$file, $destination)) { + $this->warn("Failed to publish {$file}."); + + continue; + } + + $this->line(" Published {$file}"); + } + + $ssl = $this->option('ssl'); + + if ($ssl !== null && $ssl !== '' && ! filter_var($ssl, FILTER_VALIDATE_BOOLEAN)) { + if (! copy($stubs.'/docker/nginx-no-ssl.conf', $base.'/docker/nginx.conf')) { + $this->warn('Failed to write nginx.conf (no-SSL).'); + } + } + + return self::SUCCESS; + } +} diff --git a/src/Environments/DockerEnvironment.php b/src/Environments/DockerEnvironment.php index 99c4e55..3db065d 100644 --- a/src/Environments/DockerEnvironment.php +++ b/src/Environments/DockerEnvironment.php @@ -149,37 +149,11 @@ protected function publishStubs(InstallCommand $command): void { $command->info('Publishing Docker stubs...'); - $stubs = dirname(__DIR__, 2).'/stubs/docker'; - - foreach ([ - 'docker-compose.yml', - 'docker/Dockerfile', - 'docker/nginx.conf', - 'docker/php.ini', - 'docker/xdebug.ini', - ] as $file) { - $destination = $command->path($file); - - if (file_exists($destination)) { - continue; - } - - @mkdir(dirname($destination), 0755, true); - - if (! copy($stubs.'/'.$file, $destination)) { - $command->warn("Failed to publish {$file}."); - } - } - - if (! $this->ssl) { - $copied = copy( - $stubs.'/docker/nginx-no-ssl.conf', - $command->path('docker/nginx.conf'), - ); - if (! $copied) { - $command->warn('Failed to write nginx.conf (no-SSL). Check that Docker stubs were published first.'); - } - } + // Non-interactive: existing files are kept (the confirm defaults to "no"). + $command->call('docker:publish', [ + '--path' => $command->path(), + '--ssl' => $this->ssl ? 'yes' : 'no', + ]); } protected function generateSsl(InstallCommand $command): void diff --git a/tests/Feature/PublishDockerCommandTest.php b/tests/Feature/PublishDockerCommandTest.php new file mode 100644 index 0000000..f72dc5f --- /dev/null +++ b/tests/Feature/PublishDockerCommandTest.php @@ -0,0 +1,72 @@ +tmp = sys_get_temp_dir().'/sb-docker-publish-'.uniqid(); + mkdir($this->tmp, 0755, true); + } + + protected function tearDown(): void + { + exec('rm -rf '.escapeshellarg($this->tmp)); + parent::tearDown(); + } + + public function test_it_publishes_the_docker_files(): void + { + $this->artisan("docker:publish --path={$this->tmp}")->assertSuccessful(); + + foreach (['docker-compose.yml', 'docker/Dockerfile', 'docker/nginx.conf', 'docker/php.ini', 'docker/xdebug.ini'] as $file) { + $this->assertFileExists($this->tmp.'/'.$file); + } + } + + public function test_it_keeps_existing_files_when_not_forced(): void + { + file_put_contents($this->tmp.'/docker-compose.yml', 'mine'); + + $this->artisan("docker:publish --path={$this->tmp}") + ->assertSuccessful() + ->expectsOutputToContain('Skipped docker-compose.yml'); + + $this->assertSame('mine', file_get_contents($this->tmp.'/docker-compose.yml')); + } + + public function test_force_overwrites_existing_files(): void + { + file_put_contents($this->tmp.'/docker-compose.yml', 'mine'); + + $this->artisan("docker:publish --path={$this->tmp} --force")->assertSuccessful(); + + $this->assertNotSame('mine', file_get_contents($this->tmp.'/docker-compose.yml')); + } + + public function test_ssl_no_publishes_the_plain_http_nginx_config(): void + { + $this->artisan("docker:publish --path={$this->tmp} --ssl=no")->assertSuccessful(); + + $this->assertSame( + file_get_contents(dirname(__DIR__, 2).'/stubs/docker/docker/nginx-no-ssl.conf'), + file_get_contents($this->tmp.'/docker/nginx.conf'), + ); + } + + public function test_ssl_yes_publishes_the_ssl_nginx_config(): void + { + $this->artisan("docker:publish --path={$this->tmp} --ssl=yes")->assertSuccessful(); + + $this->assertSame( + file_get_contents(dirname(__DIR__, 2).'/stubs/docker/docker/nginx.conf'), + file_get_contents($this->tmp.'/docker/nginx.conf'), + ); + } +}