diff --git a/README.md b/README.md index e2e8c06..81f79d9 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,18 @@ class AppServiceProvider extends Provider } ``` + +## Triggering Command Failures + +If you are using the SSG in a CI environment, you may want to prevent the command from succeeding if any pages aren't generated (e.g. to prevent deployment of an incomplete site). + +By default, the command will finish and exit with a success code even if there were un-generated pages. You can tell configure the SSG to fail early on errors, or even on warnings. + +```php +'failures' => 'errors', // or 'warnings' +``` + + ## Deployment Examples These examples assume your workflow will be to author content **locally** and _not_ using the control panel in production. diff --git a/config/ssg.php b/config/ssg.php index 5c3d041..a8b917a 100644 --- a/config/ssg.php +++ b/config/ssg.php @@ -90,4 +90,17 @@ 'directory' => 'img', ], + /* + |-------------------------------------------------------------------------- + | Failures + |-------------------------------------------------------------------------- + | + | You may configure whether the console command will exit early with a + | failure status code when it encounters errors or warnings. You may + | want to do this to prevent deployments in CI environments, etc. + | + */ + + 'failures' => false, // 'errors' or 'warnings' + ]; diff --git a/src/Commands/StaticSiteGenerate.php b/src/Commands/StaticSiteGenerate.php index 5bc61aa..a944a8d 100644 --- a/src/Commands/StaticSiteGenerate.php +++ b/src/Commands/StaticSiteGenerate.php @@ -5,6 +5,7 @@ use Illuminate\Console\Command; use Statamic\StaticSite\Generator; use Statamic\Console\RunsInPlease; +use Statamic\StaticSite\GenerationFailedException; use Wilderborn\Partyline\Facade as Partyline; class StaticSiteGenerate extends Command @@ -55,8 +56,16 @@ public function handle() $this->comment('You may be able to speed up site generation significantly by installing spatie/fork and using multiple workers (requires PHP 8+).'); } - $this->generator - ->workers($workers ?? 1) - ->generate(); + try { + $this->generator + ->workers($workers ?? 1) + ->generate(); + } catch (GenerationFailedException $e) { + $this->line($e->getConsoleMessage()); + $this->error('Static site generation failed.'); + return 1; + } + + return 0; } } diff --git a/src/GenerationFailedException.php b/src/GenerationFailedException.php new file mode 100644 index 0000000..dce975e --- /dev/null +++ b/src/GenerationFailedException.php @@ -0,0 +1,25 @@ +setConsoleMessage($message); + } + + public function setConsoleMessage($message) + { + $this->consoleMessage = $message; + + return $this; + } + + public function getConsoleMessage() + { + return $this->consoleMessage; + } +} diff --git a/src/Generator.php b/src/Generator.php index b53f337..fc8a807 100644 --- a/src/Generator.php +++ b/src/Generator.php @@ -31,12 +31,10 @@ class Generator protected $config; protected $request; protected $after; - protected $count = 0; - protected $skips = 0; - protected $warnings = 0; protected $viewPaths; protected $extraUrls; protected $workers = 1; + protected $taskResults; public function __construct(Application $app, Filesystem $files, Router $router, Tasks $tasks) { @@ -90,17 +88,8 @@ public function generate() ->clearDirectory() ->createContentFiles() ->createSymlinks() - ->copyFiles(); - - Partyline::info('Static site generated into ' . $this->config['destination']); - - if ($this->skips) { - Partyline::warn("[!] {$this->skips}/{$this->count} pages not generated"); - } - - if ($this->warnings) { - Partyline::warn("[!] {$this->warnings}/{$this->count} pages generated with warnings"); - } + ->copyFiles() + ->outputSummary(); if ($this->after) { call_user_func($this->after); @@ -167,6 +156,8 @@ public function copyFiles() Partyline::line("[✔] $source copied to $dest"); } + + return $this; } protected function createContentFiles() @@ -184,11 +175,33 @@ protected function createContentFiles() $results = $this->tasks->run(...$closures); - $this->outputResults($results); + if ($this->anyTasksFailed($results)) { + throw GenerationFailedException::withConsoleMessage("\x1B[1A\x1B[2K"); + } + + $this->taskResults = $this->compileTasksResults($results); + + $this->outputTasksResults(); return $this; } + protected function anyTasksFailed($results) + { + return collect($results)->contains(''); + } + + protected function compileTasksResults(array $results) + { + $results = collect($results); + + return [ + 'count' => $results->sum('count'), + 'warnings' => $results->flatMap->warnings, + 'errors' => $results->flatMap->errors, + ]; + } + protected function gatherContent() { Partyline::line('Gathering content to be generated...'); @@ -225,7 +238,8 @@ protected function makeContentGenerationClosures($pages, $request) { return $pages->split($this->workers)->map(function ($pages) use ($request) { return function () use ($pages, $request) { - $count = $skips = $warnings = 0; + $count = 0; + $warnings = []; $errors = []; foreach ($pages as $page) { @@ -242,33 +256,50 @@ protected function makeContentGenerationClosures($pages, $request) try { $generated = $page->generate($request); } catch (NotGeneratedException $e) { - $skips++; + if ($this->shouldFail($e)) { + throw GenerationFailedException::withConsoleMessage("\x1B[1A\x1B[2K".$e->consoleMessage()); + } + $errors[] = $e->consoleMessage(); continue; } if ($generated->hasWarning()) { - $warnings++; + if ($this->shouldFail($generated)) { + throw GenerationFailedException::withConsoleMessage($generated->consoleMessage()); + } + + $warnings[] = $generated->consoleMessage(); } } - return compact('count', 'skips', 'warnings', 'errors'); + return compact('count', 'warnings', 'errors'); }; })->all(); } - protected function outputResults($results) + protected function outputTasksResults() { - $results = collect($results); + $results = $this->taskResults; + + Partyline::line("\x1B[1A\x1B[2K[✔] Generated {$results['count']} content files"); + + $results['warnings']->merge($results['errors'])->each(fn ($error) => Partyline::line($error)); + } + + protected function outputSummary() + { + Partyline::info(''); + Partyline::info('Static site generated into ' . $this->config['destination']); + + $total = $this->taskResults['count']; - Partyline::line("\x1B[1A\x1B[2K[✔] Generated {$results->sum('count')} content files"); + if ($errors = count($this->taskResults['errors'])) { + Partyline::warn("[!] {$errors}/{$total} pages not generated"); + } - if ($results->sum('skips')) { - $results->reduce(function ($carry, $item) { - return $carry->merge($item['errors']); - }, collect())->each(function ($error) { - Partyline::line($error); - }); + if ($warnings = count($this->taskResults['warnings'])) { + Partyline::warn("[!] {$warnings}/{$total} pages generated with warnings"); } } @@ -367,4 +398,15 @@ protected function checkConcurrencySupport() throw new \RuntimeException('To use multiple workers, you must install PHP 8 and spatie/fork.'); } + + protected function shouldFail($item) + { + $config = $this->config['failures']; + + if ($item instanceof NotGeneratedException) { + return in_array($config, ['warnings', 'errors']); + } + + return $config === 'warnings'; + } }