From 48f0bc710dfc4c2e3b1bb13b8a1e34c6d781a448 Mon Sep 17 00:00:00 2001 From: Sabrina Bourouis Date: Wed, 8 Oct 2025 10:57:10 +1100 Subject: [PATCH 1/3] Create deploy command --- config/nightwatch.php | 1 + src/Console/DeployCommand.php | 77 +++++++++++++++++++++ src/NightwatchServiceProvider.php | 12 ++++ tests/Feature/Console/DeployCommandTest.php | 48 +++++++++++++ 4 files changed, 138 insertions(+) create mode 100644 src/Console/DeployCommand.php create mode 100644 tests/Feature/Console/DeployCommandTest.php diff --git a/config/nightwatch.php b/config/nightwatch.php index 60d10a8b..807f882f 100644 --- a/config/nightwatch.php +++ b/config/nightwatch.php @@ -5,6 +5,7 @@ 'token' => env('NIGHTWATCH_TOKEN'), 'deployment' => env('NIGHTWATCH_DEPLOY'), 'server' => env('NIGHTWATCH_SERVER', (string) gethostname()), + 'base_url' => env('NIGHTWATCH_BASE_URL', 'https://nightwatch.laravel.com'), 'capture_exception_source_code' => env('NIGHTWATCH_CAPTURE_EXCEPTION_SOURCE_CODE', true), 'redact_headers' => explode(',', env('NIGHTWATCH_REDACT_HEADERS', 'Authorization,Cookie,Proxy-Authorization,X-XSRF-TOKEN')), diff --git a/src/Console/DeployCommand.php b/src/Console/DeployCommand.php new file mode 100644 index 00000000..dc8fec72 --- /dev/null +++ b/src/Console/DeployCommand.php @@ -0,0 +1,77 @@ +baseUrl) { + $this->error('No Nightwatch base URL configured.'); + + return; + } + + if (! $this->token) { + $this->error('No Nightwatch token configured.'); + + return; + } + + try { + $response = $this->http + ->withHeaders([ + 'Authorization' => "Bearer {$this->token}", + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + ]) + ->post("{$this->baseUrl}/api/deployments", [ + 'timestamp' => now()->timestamp, + 'version' => $tag, + ]); + + if ($response->successful()) { + $this->info('Deployment successful'); + } else { + $this->error("Deployment failed: {$response->status()} {$response->body()}"); + } + } catch (Exception $e) { + $this->error("Deployment failed: {$e->getMessage()}"); + } + } +} diff --git a/src/NightwatchServiceProvider.php b/src/NightwatchServiceProvider.php index c40c38ec..d58edd80 100644 --- a/src/NightwatchServiceProvider.php +++ b/src/NightwatchServiceProvider.php @@ -40,6 +40,7 @@ use Illuminate\Support\Facades\Context; use Illuminate\Support\ServiceProvider; use Laravel\Nightwatch\Console\AgentCommand; +use Laravel\Nightwatch\Console\DeployCommand; use Laravel\Nightwatch\Facades\Nightwatch; use Laravel\Nightwatch\Factories\Logger; use Laravel\Nightwatch\Hooks\ArtisanStartingListener; @@ -192,6 +193,7 @@ private function registerBindings(): void $this->registerLogger(); $this->registerMiddleware(); $this->registerAgentCommand(); + $this->registerDeployCommand(); $this->buildAndRegisterCore(); } @@ -226,6 +228,15 @@ private function registerAgentCommand(): void )); } + private function registerDeployCommand(): void + { + $this->app->singleton(DeployCommand::class, fn () => new DeployCommand( + http: $this->app->make(\Illuminate\Http\Client\Factory::class), + token: $this->nightwatchConfig['token'] ?? null, + baseUrl: $this->nightwatchConfig['base_url'] ?? null, + )); + } + private function buildAndRegisterCore(): void { $clock = new Clock; @@ -295,6 +306,7 @@ private function registerCommands(): void $this->commands([ Console\AgentCommand::class, Console\StatusCommand::class, + Console\DeployCommand::class, ]); } diff --git a/tests/Feature/Console/DeployCommandTest.php b/tests/Feature/Console/DeployCommandTest.php new file mode 100644 index 00000000..b0c715ba --- /dev/null +++ b/tests/Feature/Console/DeployCommandTest.php @@ -0,0 +1,48 @@ +start('NIGHTWATCH_TOKEN="test-token" \ + NIGHTWATCH_BASE_URL="http://localhost" \ + NIGHTWATCH_DEPLOY="v1.2.3" \ + vendor/bin/testbench nightwatch:deploy' + ); + + try { + $result = $process->wait(function ($type, $o) use (&$output, $process) { + $output .= $o; + + $process->signal(SIGTERM); + + $tries = 0; + + while ($tries < 3) { + if (! $process->running()) { + return; + } + + $tries++; + sleep(1); + } + + $process->signal(SIGKILL); + }); + } catch (ProcessTimedOutException $e) { + throw new RuntimeException('Failed to deploy or stop the agent running. Output:'.PHP_EOL.$output, previous: $e); + } + + $this->assertStringContainsString('Deployment successful', $output); + } +} From b389d11eb386e3f0215aebddf060f7e336ec0078 Mon Sep 17 00:00:00 2001 From: Sabrina Bourouis Date: Wed, 8 Oct 2025 15:42:58 +1100 Subject: [PATCH 2/3] Remove baseUrl parameter from DeployCommand Co-authored-by: Jess Archer --- config/nightwatch.php | 1 - src/Console/DeployCommand.php | 13 ++++--------- src/NightwatchServiceProvider.php | 1 - tests/Feature/Console/DeployCommandTest.php | 1 - 4 files changed, 4 insertions(+), 12 deletions(-) diff --git a/config/nightwatch.php b/config/nightwatch.php index 807f882f..60d10a8b 100644 --- a/config/nightwatch.php +++ b/config/nightwatch.php @@ -5,7 +5,6 @@ 'token' => env('NIGHTWATCH_TOKEN'), 'deployment' => env('NIGHTWATCH_DEPLOY'), 'server' => env('NIGHTWATCH_SERVER', (string) gethostname()), - 'base_url' => env('NIGHTWATCH_BASE_URL', 'https://nightwatch.laravel.com'), 'capture_exception_source_code' => env('NIGHTWATCH_CAPTURE_EXCEPTION_SOURCE_CODE', true), 'redact_headers' => explode(',', env('NIGHTWATCH_REDACT_HEADERS', 'Authorization,Cookie,Proxy-Authorization,X-XSRF-TOKEN')), diff --git a/src/Console/DeployCommand.php b/src/Console/DeployCommand.php index dc8fec72..80f09007 100644 --- a/src/Console/DeployCommand.php +++ b/src/Console/DeployCommand.php @@ -14,7 +14,7 @@ /** * @internal */ -#[AsCommand(name: 'nightwatch:deploy', description: 'Notice the Nightwatch agent that a new deployment has been made.')] +#[AsCommand(name: 'nightwatch:deploy', description: 'Notify Nightwatch of a deployment.')] final class DeployCommand extends Command { /** @@ -27,11 +27,10 @@ final class DeployCommand extends Command /** * @var string */ - protected $description = 'Notice the Nightwatch agent that a new deployment has been made.'; + protected $description = 'Notify Nightwatch of a deployment.'; public function __construct( private HttpFactory $http, - private ?string $baseUrl, #[SensitiveParameter] private ?string $token, ) { parent::__construct(); @@ -41,11 +40,7 @@ public function handle(): void { $tag = config('nightwatch.deployment') ?? ''; - if (! $this->baseUrl) { - $this->error('No Nightwatch base URL configured.'); - - return; - } + $baseUrl = $_SERVER['NIGHTWATCH_BASE_URL'] ?? 'https://nightwatch.laravel.com'; if (! $this->token) { $this->error('No Nightwatch token configured.'); @@ -60,7 +55,7 @@ public function handle(): void 'Accept' => 'application/json', 'Content-Type' => 'application/json', ]) - ->post("{$this->baseUrl}/api/deployments", [ + ->post("{$baseUrl}/api/deployments", [ 'timestamp' => now()->timestamp, 'version' => $tag, ]); diff --git a/src/NightwatchServiceProvider.php b/src/NightwatchServiceProvider.php index d58edd80..21265149 100644 --- a/src/NightwatchServiceProvider.php +++ b/src/NightwatchServiceProvider.php @@ -233,7 +233,6 @@ private function registerDeployCommand(): void $this->app->singleton(DeployCommand::class, fn () => new DeployCommand( http: $this->app->make(\Illuminate\Http\Client\Factory::class), token: $this->nightwatchConfig['token'] ?? null, - baseUrl: $this->nightwatchConfig['base_url'] ?? null, )); } diff --git a/tests/Feature/Console/DeployCommandTest.php b/tests/Feature/Console/DeployCommandTest.php index b0c715ba..6e96d4b4 100644 --- a/tests/Feature/Console/DeployCommandTest.php +++ b/tests/Feature/Console/DeployCommandTest.php @@ -15,7 +15,6 @@ public function test_it_can_run_the_agent_command(): void { $output = ''; $process = Process::timeout(10)->start('NIGHTWATCH_TOKEN="test-token" \ - NIGHTWATCH_BASE_URL="http://localhost" \ NIGHTWATCH_DEPLOY="v1.2.3" \ vendor/bin/testbench nightwatch:deploy' ); From 4c45d13ae42b365f0e602845fb875335e4d9b13c Mon Sep 17 00:00:00 2001 From: Sabrina Bourouis Date: Tue, 14 Oct 2025 15:39:31 +1100 Subject: [PATCH 3/3] Improve deploy command and add tests --- src/Console/DeployCommand.php | 54 ++++++++++++------- src/NightwatchServiceProvider.php | 1 - tests/Feature/Console/DeployCommandTest.php | 60 +++++++++++++++++++-- 3 files changed, 92 insertions(+), 23 deletions(-) diff --git a/src/Console/DeployCommand.php b/src/Console/DeployCommand.php index 80f09007..5f4df124 100644 --- a/src/Console/DeployCommand.php +++ b/src/Console/DeployCommand.php @@ -2,14 +2,16 @@ namespace Laravel\Nightwatch\Console; -use Exception; +use Carbon\CarbonImmutable; use Illuminate\Console\Command; -use Illuminate\Http\Client\Factory as HttpFactory; +use Illuminate\Support\Facades\Http; use SensitiveParameter; use Symfony\Component\Console\Attribute\AsCommand; +use Throwable; use function config; -use function now; +use function strlen; +use function substr; /** * @internal @@ -22,51 +24,65 @@ final class DeployCommand extends Command */ protected $signature = 'nightwatch:deploy'; - protected $hidden = true; - /** * @var string */ protected $description = 'Notify Nightwatch of a deployment.'; + /** + * @var bool + */ + protected $hidden = true; + public function __construct( - private HttpFactory $http, #[SensitiveParameter] private ?string $token, ) { parent::__construct(); } - public function handle(): void + public function handle(): int { - $tag = config('nightwatch.deployment') ?? ''; - - $baseUrl = $_SERVER['NIGHTWATCH_BASE_URL'] ?? 'https://nightwatch.laravel.com'; - if (! $this->token) { - $this->error('No Nightwatch token configured.'); + $this->error('No NIGHTWATCH_TOKEN environment variable configured.'); - return; + return 1; } + $tag = config('nightwatch.deployment') ?? ''; + + $baseUrl = $_SERVER['NIGHTWATCH_BASE_URL'] ?? 'https://nightwatch.laravel.com'; + try { - $response = $this->http + $response = Http::connectTimeout(5) + ->timeout(10) ->withHeaders([ 'Authorization' => "Bearer {$this->token}", 'Accept' => 'application/json', - 'Content-Type' => 'application/json', ]) ->post("{$baseUrl}/api/deployments", [ - 'timestamp' => now()->timestamp, + 'timestamp' => CarbonImmutable::now()->timestamp, 'version' => $tag, ]); if ($response->successful()) { $this->info('Deployment successful'); + + return 0; } else { - $this->error("Deployment failed: {$response->status()} {$response->body()}"); + $message = $response->body(); + + if (strlen($message) > 1005) { + $message = substr($message, 0, 1000).'[...]'; + } + + $this->error("Deployment failed: {$response->status()} [{$message}]"); + + return 1; } - } catch (Exception $e) { - $this->error("Deployment failed: {$e->getMessage()}"); + } catch (Throwable $e) { + $this->error("Deployment failed: [{$e->getMessage()}]"); + + return 1; } } } diff --git a/src/NightwatchServiceProvider.php b/src/NightwatchServiceProvider.php index 21265149..d18e6bb1 100644 --- a/src/NightwatchServiceProvider.php +++ b/src/NightwatchServiceProvider.php @@ -231,7 +231,6 @@ private function registerAgentCommand(): void private function registerDeployCommand(): void { $this->app->singleton(DeployCommand::class, fn () => new DeployCommand( - http: $this->app->make(\Illuminate\Http\Client\Factory::class), token: $this->nightwatchConfig['token'] ?? null, )); } diff --git a/tests/Feature/Console/DeployCommandTest.php b/tests/Feature/Console/DeployCommandTest.php index 6e96d4b4..e9388453 100644 --- a/tests/Feature/Console/DeployCommandTest.php +++ b/tests/Feature/Console/DeployCommandTest.php @@ -3,6 +3,7 @@ namespace Tests\Feature\Console; use Illuminate\Process\Exceptions\ProcessTimedOutException; +use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Process; use RuntimeException; use Tests\TestCase; @@ -11,11 +12,10 @@ class DeployCommandTest extends TestCase { - public function test_it_can_run_the_agent_command(): void + public function test_it_can_run_the_deploy_command(): void { $output = ''; - $process = Process::timeout(10)->start('NIGHTWATCH_TOKEN="test-token" \ - NIGHTWATCH_DEPLOY="v1.2.3" \ + $process = Process::timeout(10)->start('NIGHTWATCH_DEPLOY="v1.2.3" \ vendor/bin/testbench nightwatch:deploy' ); @@ -44,4 +44,58 @@ public function test_it_can_run_the_agent_command(): void $this->assertStringContainsString('Deployment successful', $output); } + + public function test_it_fails_when_the_deploy_command_is_run_without_a_token(): void + { + $process = Process::timeout(10)->start('NIGHTWATCH_DEPLOY="v1.2.3" \ + NIGHTWATCH_TOKEN="" \ + vendor/bin/testbench nightwatch:deploy'); + + try { + $process->wait(function ($type, $o) use (&$output, $process) { + $output .= $o; + + $process->signal(SIGTERM); + + $tries = 0; + + while ($tries < 3) { + if (! $process->running()) { + return; + } + + $tries++; + sleep(1); + } + + $process->signal(SIGKILL); + }); + } catch (ProcessTimedOutException $e) { + throw new RuntimeException('Failed to deploy or stop the agent running. Output:'.PHP_EOL.$output, previous: $e); + } + + $this->assertStringContainsString('No NIGHTWATCH_TOKEN environment variable configured.', $process->output()); + } + + public function test_it_handles_http_errors(): void + { + Http::fake([ + $_SERVER['NIGHTWATCH_BASE_URL'].'/api/deployments' => Http::response('Whoops!', 500), + ]); + + $this->artisan('nightwatch:deploy') + ->expectsOutput('Deployment failed: 500 [Whoops!]') + ->assertExitCode(1); + } + + public function test_it_handles_throwable_errors(): void + { + Http::fake([ + $_SERVER['NIGHTWATCH_BASE_URL'].'/api/deployments' => Http::failedConnection('Whoops!'), + ]); + + $this->artisan('nightwatch:deploy') + ->expectsOutput('Deployment failed: [Whoops!]') + ->assertExitCode(1); + } }