From fbe4a32ad2d5ad5bef3fc048004cf8df2a7068b6 Mon Sep 17 00:00:00 2001 From: KodeStar Date: Sat, 3 Oct 2026 14:01:01 +0100 Subject: [PATCH 1/2] fix: register the register:app artisan command reliably AppServiceProvider::boot() calls Artisan (storage:link, migrate, key:generate). An Artisan call made while providers are still booting loads the console commands before the framework's booted callback adds app/Console/Commands to the discovery paths, so register:app was never registered for that process. In the Docker image this happened on every run: the storage:link guard looked for public/storage/.gitignore, which the image does not ship, so storage:link ran on every boot (and every web request). - Register RegisterApp explicitly in AppServiceProvider::register(), which runs before any boot-time Artisan call. - Guard storage:link on the public/storage link itself. Fixes #1606 --- app/Providers/AppServiceProvider.php | 11 ++++++++++- tests/Feature/ConsoleCommandsTest.php | 25 +++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 tests/Feature/ConsoleCommandsTest.php diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 7c562d59b..0b0c957a9 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -3,6 +3,7 @@ namespace App\Providers; use App\Application; +use App\Console\Commands\RegisterApp; use App\Jobs\ProcessApps; use App\Jobs\UpdateApps; use App\Setting; @@ -33,7 +34,10 @@ public function boot(): void $this->setupDatabase(); - if (! is_file(public_path('storage/.gitignore'))) { + // Check the link itself, not a file inside it: the Docker image does + // not ship storage/app/public/.gitignore, so testing for that ran + // storage:link on every request. + if (! file_exists(public_path('storage'))) { Artisan::call('storage:link'); \Session::put('current_user', null); } @@ -136,6 +140,11 @@ public function genKey(): void */ public function register(): void { + // Registered explicitly as well as through app/Console/Commands + // discovery: an Artisan::call() in boot() loads the console commands + // before that path is added, which drops register:app (#1606). + $this->commands([RegisterApp::class]); + if ($this->app->isLocal()) { $this->app->register(IdeHelperServiceProvider::class); } diff --git a/tests/Feature/ConsoleCommandsTest.php b/tests/Feature/ConsoleCommandsTest.php new file mode 100644 index 000000000..28c0ed333 --- /dev/null +++ b/tests/Feature/ConsoleCommandsTest.php @@ -0,0 +1,25 @@ +assertArrayHasKey('register:app', $this->app->make(Kernel::class)->all()); + } +} From 266c9705893402974bf25593fb1e2d77ade8b053 Mon Sep 17 00:00:00 2001 From: KodeStar Date: Sat, 3 Oct 2026 14:13:44 +0100 Subject: [PATCH 2/2] refactor: register console command paths early instead of listing classes Adding app/Console/Commands and routes/console.php to the console kernel in register() fixes discovery for every command, including closure commands in routes/console.php, rather than only for RegisterApp. The test now checks every command in app/Console/Commands is registered. --- app/Providers/AppServiceProvider.php | 18 ++++++++++-------- tests/Feature/ConsoleCommandsTest.php | 27 ++++++++++++++++----------- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 0b0c957a9..1df58b35d 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -3,12 +3,12 @@ namespace App\Providers; use App\Application; -use App\Console\Commands\RegisterApp; use App\Jobs\ProcessApps; use App\Jobs\UpdateApps; use App\Setting; use App\User; use Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider; +use Illuminate\Contracts\Console\Kernel as ConsoleKernel; use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Log; @@ -34,9 +34,7 @@ public function boot(): void $this->setupDatabase(); - // Check the link itself, not a file inside it: the Docker image does - // not ship storage/app/public/.gitignore, so testing for that ran - // storage:link on every request. + // Check the link itself; the Docker image doesn't ship storage/app/public/.gitignore. if (! file_exists(public_path('storage'))) { Artisan::call('storage:link'); \Session::put('current_user', null); @@ -140,10 +138,14 @@ public function genKey(): void */ public function register(): void { - // Registered explicitly as well as through app/Console/Commands - // discovery: an Artisan::call() in boot() loads the console commands - // before that path is added, which drops register:app (#1606). - $this->commands([RegisterApp::class]); + // The framework only adds these paths once providers have booted, but + // an Artisan::call() in boot() loads the console commands before + // then, which dropped register:app (#1606). Add them up front. + if ($this->app->runningInConsole()) { + $this->app->make(ConsoleKernel::class) + ->addCommandPaths([app_path('Console/Commands')]) + ->addCommandRoutePaths([base_path('routes/console.php')]); + } if ($this->app->isLocal()) { $this->app->register(IdeHelperServiceProvider::class); diff --git a/tests/Feature/ConsoleCommandsTest.php b/tests/Feature/ConsoleCommandsTest.php index 28c0ed333..2d42f4f18 100644 --- a/tests/Feature/ConsoleCommandsTest.php +++ b/tests/Feature/ConsoleCommandsTest.php @@ -2,24 +2,29 @@ namespace Tests\Feature; +use Illuminate\Console\Command; use Illuminate\Contracts\Console\Kernel; use Tests\TestCase; /** - * Guards registration of the app's own artisan commands. - * - * AppServiceProvider::boot() can call Artisan (migrate on a fresh or upgraded - * database, key:generate, storage:link). An Artisan call made while providers - * are still booting loads the console kernel's commands before the framework - * has added app/Console/Commands to its discovery paths, so path discovery - * alone silently loses register:app for that process (#1606). The test - * database is always fresh, so every test boot goes through that migrate - * call. + * Guards against #1606: an Artisan call in AppServiceProvider::boot() dropped + * the commands in app/Console/Commands. The test database is always fresh, so + * every test boot runs the boot-time migrate that triggers it. */ class ConsoleCommandsTest extends TestCase { - public function test_register_app_command_is_registered(): void + public function test_every_command_in_app_console_commands_is_registered(): void { - $this->assertArrayHasKey('register:app', $this->app->make(Kernel::class)->all()); + $registered = array_map('get_class', $this->app->make(Kernel::class)->all()); + + $files = glob(app_path('Console/Commands/*.php')); + $this->assertNotEmpty($files); + + foreach ($files as $file) { + $class = 'App\\Console\\Commands\\'.basename($file, '.php'); + if (is_subclass_of($class, Command::class)) { + $this->assertContains($class, $registered); + } + } } }