diff --git a/src/Illuminate/Console/Scheduling/ManagesAttributes.php b/src/Illuminate/Console/Scheduling/ManagesAttributes.php index 1a18378eb62f..34e34b5babc6 100644 --- a/src/Illuminate/Console/Scheduling/ManagesAttributes.php +++ b/src/Illuminate/Console/Scheduling/ManagesAttributes.php @@ -2,6 +2,7 @@ namespace Illuminate\Console\Scheduling; +use Illuminate\Support\Arr; use Illuminate\Support\Reflector; trait ManagesAttributes @@ -97,6 +98,13 @@ trait ManagesAttributes */ public $description; + /** + * The allowed exit codes additional to 0. + * + * @var array|true + */ + public array|true $allowExitCodes = []; + /** * Set which user the command should run as. * @@ -110,6 +118,31 @@ public function user($user) return $this; } + /** + * Set which exit codes should be accepted additional to 0. + * + * @param int|array $exitCode + * @return $this + */ + public function allowExitCode(int|array $exitCode): static + { + $this->allowExitCodes = Arr::wrap($exitCode); + + return $this; + } + + /** + * Accept all exit codes as successful. + * + * @return $this + */ + public function allowFailure(): static + { + $this->allowExitCodes = true; + + return $this; + } + /** * Limit the environments the command should run in. * diff --git a/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php b/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php index 0f0ad16d2c46..df5915170c80 100644 --- a/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php +++ b/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php @@ -2,7 +2,6 @@ namespace Illuminate\Console\Scheduling; -use Exception; use Illuminate\Console\Application; use Illuminate\Console\Command; use Illuminate\Console\Events\ScheduledTaskFailed; @@ -203,8 +202,8 @@ protected function runEvent($event) $this->eventsRan = true; - if ($event->exitCode != 0 && ! $event->runInBackground) { - throw new Exception("Scheduled command [{$event->command}] failed with exit code [{$event->exitCode}]."); + if ($this->shouldFailOnExitCode($event) && ! $event->runInBackground) { + throw new UnexpectedExitCodeException($event->command, $event->exitCode); } } catch (Throwable $e) { $this->dispatcher->dispatch(new ScheduledTaskFailed($event, $e)); @@ -286,4 +285,17 @@ protected function clearInterruptSignal() { $this->cache->forget('illuminate:schedule:interrupt'); } + + /** + * Determine if the scheduled command should be considered failed based on its exit code. + * + * @param \Illuminate\Console\Scheduling\Event $event + * @return bool + */ + protected function shouldFailOnExitCode(Event $event): bool + { + return $event->allowExitCodes !== true && + $event->exitCode != 0 && + ! in_array($event->exitCode, $event->allowExitCodes); + } } diff --git a/src/Illuminate/Console/Scheduling/UnexpectedExitCodeException.php b/src/Illuminate/Console/Scheduling/UnexpectedExitCodeException.php new file mode 100644 index 000000000000..6e9ca0867146 --- /dev/null +++ b/src/Illuminate/Console/Scheduling/UnexpectedExitCodeException.php @@ -0,0 +1,13 @@ +app->make(Schedule::class); + $task = $schedule->exec('exit 1') + ->everyMinute() + ->allowExitCode(1); + + // Make sure it will run regardless of schedule + $task->when(function () { + return true; + }); + + // Execute the scheduler + $this->artisan('schedule:run'); + + // Verify the event sequence + Event::assertDispatched(ScheduledTaskStarting::class); + Event::assertDispatched(ScheduledTaskFinished::class); + Event::assertNotDispatched(ScheduledTaskFailed::class); + } + + /** + * @throws BindingResolutionException + */ + public function test_failing_command_with_allowed_failures_in_foreground_does_not_trigger_event() + { + Event::fake([ + ScheduledTaskStarting::class, + ScheduledTaskFinished::class, + ScheduledTaskFailed::class, + ]); + + // Create a schedule and add the command + $schedule = $this->app->make(Schedule::class); + $task = $schedule->exec('exit 1') + ->everyMinute() + ->allowFailure(); + + // Make sure it will run regardless of schedule + $task->when(function () { + return true; + }); + + // Execute the scheduler + $this->artisan('schedule:run'); + + // Verify the event sequence + Event::assertDispatched(ScheduledTaskStarting::class); + Event::assertDispatched(ScheduledTaskFinished::class); + Event::assertNotDispatched(ScheduledTaskFailed::class); + } + /** * @throws BindingResolutionException */