diff --git a/app/Commands/DeployCommand.php b/app/Commands/DeployCommand.php
index efc1a47..68787f2 100644
--- a/app/Commands/DeployCommand.php
+++ b/app/Commands/DeployCommand.php
@@ -17,7 +17,7 @@ class DeployCommand extends Command
{
use EnsureHasToken, HasPloiConfiguration, InteractWithServer, InteractWithSite;
- protected $signature = 'deploy {--server=} {--site=} {--schedule=} {--stream : Stream deployment logs in real-time} {--deployment-id= : Specific deployment ID to stream}';
+ protected $signature = 'deploy {--server=} {--site=} {--schedule=} {--no-stream : Disable real-time deployment log streaming}';
protected $description = 'Deploy your site to Ploi.io with optional log streaming.';
@@ -34,7 +34,7 @@ public function handle(): void
$data = [];
$isScheduled = false;
- if ($this->ploi->getSiteDetails($serverId, $siteId)['data']['has_staging']) {
+ if ($this->site['has_staging']) {
$this->warn("{$this->site['domain']} has a staging environment.");
$deployToProduction = confirm(
label: 'Do you want to deploy to production? (yes/no)',
@@ -58,17 +58,6 @@ public function handle(): void
}
$this->deploy($serverId, $siteId, $this->site['domain'], $data, $isScheduled);
-
- // Handle streaming after deployment
- if ($this->option('stream') && ! $isScheduled) {
- $deploymentId = $this->option('deployment-id') ?? $this->getLatestDeploymentId($serverId, $siteId);
-
- if ($deploymentId) {
- $this->streamDeploymentLogs($serverId, $siteId, $deploymentId);
- } else {
- $this->error('No deployment found to stream');
- }
- }
}
protected function validateScheduleDatetime(string $datetime): void
@@ -106,21 +95,27 @@ protected function deploy($serverId, $siteId, $domain, $data, $isScheduled = fal
return;
}
- $this->pollDeploymentStatus($serverId, $siteId, $domain);
+ if ($this->option('no-stream')) {
+ $this->pollDeploymentStatus($serverId, $siteId, $domain);
+
+ return;
+ }
+
+ $this->streamAndAwait($serverId, $siteId, $domain);
}
protected function pollDeploymentStatus(string $serverId, string $siteId, string $domain): void
{
- $maxAttempts = 60; // Maximum number of polling attempts (10 minutes total with 10-second delay)
- $delay = 5; // Delay between each attempt in seconds
+ $maxAttempts = 60; // Maximum number of polling attempts (5 minutes total with 5-second delay)
+ $delay = 5; // Delay between each attempt in seconds
$this->info('Deployment initiated!');
+
$status = spin(
callback: function () use ($serverId, $siteId, $domain, $maxAttempts, $delay) {
for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
$deploymentStatus = $this->ploi->getSiteDetails($serverId, $siteId)['data']['status'] ?? 'deploying';
- // If we get a final status, return it
if (in_array($deploymentStatus, ['active', 'deploy-failed'])) {
return [
'status' => $deploymentStatus,
@@ -131,7 +126,6 @@ protected function pollDeploymentStatus(string $serverId, string $siteId, string
sleep($delay);
}
- // If we've exceeded max attempts, return timeout status
return [
'status' => 'timeout',
'domain' => $domain,
@@ -140,59 +134,41 @@ protected function pollDeploymentStatus(string $serverId, string $siteId, string
message: 'Checking deployment status...'
);
- // Handle the deployment result
match ($status['status']) {
'active' => $this->handleSuccessfulDeployment($serverId, $siteId, $status['domain']),
'deploy-failed' => $this->handleFailedDeployment($serverId, $siteId),
'timeout' => $this->warn('Deployment status check timed out. Please check manually.'),
- default => $this->warn('Deployment status is unknown. Please check manually.')
+ default => $this->warn('Deployment status is unknown. Please check manually.'),
};
}
- /**
- * Stream deployment logs in real-time
- *
- * @return void
- */
- private function streamDeploymentLogs(int $serverId, int $siteId, int $deploymentId)
+ private function streamAndAwait(int $serverId, int $siteId, string $domain): void
{
$this->info('🔄 Streaming deployment logs...');
$this->newLine();
- $poller = new DeploymentLogPoller(config('ploi.token'));
+ $poller = new DeploymentLogPoller($this->ploi);
try {
- $poller->pollDeploymentLogs($serverId, $siteId, $deploymentId, function ($line) {
- // Format the log line with timestamp
+ $status = $poller->pollDeploymentLogs($serverId, $siteId, function ($line) {
$timestamp = now()->format('H:i:s');
- $this->line("[{$timestamp}] {$line}");
+ $this->line("[{$timestamp}]> {$line}");
});
-
- $this->newLine();
- $this->success('✅ Deployment streaming completed!');
-
} catch (Exception $e) {
$this->newLine();
- $this->error('❌ Streaming failed: '.$e->getMessage());
- }
- }
+ $this->warn('Log streaming failed ('.$e->getMessage().'), falling back to status checks...');
+ $this->pollDeploymentStatus($serverId, $siteId, $domain);
- /**
- * Get the latest deployment ID for streaming
- */
- private function getLatestDeploymentId(int $serverId, int $siteId): ?int
- {
- $poller = new DeploymentLogPoller(config('ploi.token'));
-
- try {
- $deployment = $poller->getLatestDeployment($serverId, $siteId);
+ return;
+ }
- return isset($deployment['id']) ? (int) $deployment['id'] : null;
- } catch (Exception $e) {
- $this->error('Failed to get latest deployment: '.$e->getMessage());
+ $this->newLine();
- return null;
- }
+ match ($status) {
+ 'active' => $this->handleSuccessfulDeployment($serverId, $siteId, $domain),
+ 'deploy-failed' => $this->handleFailedDeployment($serverId, $siteId),
+ default => $this->warn('Deployment status check timed out. Please check manually.'),
+ };
}
/**
@@ -210,8 +186,8 @@ private function handleFailedDeployment(string $serverId, string $siteId): void
{
$this->error('Your recent deployment has failed, please check recent deploy log for errors.');
- // Show link to deployment logs if not streaming
- if (! $this->option('stream')) {
+ // When streaming, the failed log output is already shown inline.
+ if ($this->option('no-stream')) {
$this->showLogLink($serverId, $siteId);
}
}
@@ -222,7 +198,6 @@ private function handleFailedDeployment(string $serverId, string $siteId): void
private function showLogLink(string $serverId, string $siteId): void
{
try {
- // Get the latest deployment log ID
$logs = $this->ploi->getSiteLogs($serverId, $siteId, 1)['data'];
if (! empty($logs)) {
diff --git a/app/Commands/LogsCommand.php b/app/Commands/LogsCommand.php
index 8dfb622..3c5fb81 100644
--- a/app/Commands/LogsCommand.php
+++ b/app/Commands/LogsCommand.php
@@ -10,7 +10,7 @@ class LogsCommand extends Command
{
use EnsureHasToken;
- protected $signature = 'logs:stream {server} {site} {--deployment-id= : Specific deployment ID to stream}';
+ protected $signature = 'logs:stream {server} {site}';
protected $description = 'Stream deployment logs for a specific site';
@@ -20,39 +20,37 @@ public function handle(): void
$serverId = (int) $this->argument('server');
$siteId = (int) $this->argument('site');
- $deploymentId = $this->option('deployment-id');
-
- $poller = new DeploymentLogPoller(config('ploi.token'));
try {
- // If no deployment ID provided, get the latest or active deployment
- if (! $deploymentId) {
- $deployment = $poller->getActiveDeployment($serverId, $siteId)
- ?? $poller->getLatestDeployment($serverId, $siteId);
+ $site = $this->ploi->getSiteDetails($serverId, $siteId)['data'] ?? null;
+
+ if (! $site) {
+ $this->error('Site not found.');
+
+ return;
+ }
- if (! $deployment) {
- $this->error('No deployment found for this site');
+ $currentLog = $site['current_deploy_log'] ?? null;
+ $status = $site['status'] ?? null;
- return;
- }
+ if ($currentLog === null && $status !== 'deploying') {
+ $this->info('No deployment in progress for this site.');
- $deploymentId = $deployment['id'];
- $this->info("Streaming logs for deployment #{$deploymentId} (status: {$deployment['status']})");
- } else {
- $this->info("Streaming logs for deployment #{$deploymentId}");
+ return;
}
- $this->info('🔄 Streaming deployment logs... (Press Ctrl+C to stop)');
+ $this->info("🔄 Streaming deployment logs for {$site['domain']}... (Press Ctrl+C to stop)");
$this->newLine();
- $poller->pollDeploymentLogs($serverId, $siteId, $deploymentId, function ($line) {
+ $poller = new DeploymentLogPoller($this->ploi);
+
+ $poller->pollDeploymentLogs($serverId, $siteId, function ($line) {
$timestamp = now()->format('H:i:s');
- $this->line("[{$timestamp}] {$line}");
+ $this->line("[{$timestamp}]> {$line}");
});
$this->newLine();
$this->success('✅ Deployment log streaming completed!');
-
} catch (Exception $e) {
$this->newLine();
$this->error('❌ Streaming failed: '.$e->getMessage());
diff --git a/app/Services/DeploymentLogPoller.php b/app/Services/DeploymentLogPoller.php
index 6a6c9c9..d0ae9ff 100644
--- a/app/Services/DeploymentLogPoller.php
+++ b/app/Services/DeploymentLogPoller.php
@@ -3,214 +3,161 @@
namespace App\Services;
use Exception;
-use Illuminate\Support\Facades\Http;
class DeploymentLogPoller
{
- private $apiKey;
+ private const POLL_INTERVAL = 2;
- private $baseUrl;
+ private const MAX_POLL_ATTEMPTS = 300;
- public function __construct(string $apiKey, ?string $baseUrl = null)
- {
- $this->apiKey = $apiKey;
- $this->baseUrl = $baseUrl ?? config('ploi.api_url');
- }
+ private const MAX_RETRIES = 3;
+
+ private const RETRY_BACKOFF = 5;
+
+ private const FINAL_STATUSES = ['active', 'deploy-failed'];
+
+ public function __construct(private readonly PloiAPI $ploi) {}
/**
- * Poll deployment logs and stream new lines as they appear
+ * Poll the site endpoint, streaming new lines from `current_deploy_log` as they appear.
*
- * @param callable|null $onNewLines Callback for each new log line
+ * Returns the site's final status (`active` / `deploy-failed`), or `null` on timeout.
+ *
+ * @param callable|null $onNewLines Receives each new non-empty log line.
*
* @throws Exception
*/
- public function pollDeploymentLogs(int $serverId, int $siteId, int $deploymentId, ?callable $onNewLines = null): void
+ public function pollDeploymentLogs(int $serverId, int $siteId, ?callable $onNewLines = null): ?string
{
- $isActive = true;
$lastLogPosition = 0;
- $pollInterval = 2; // seconds
- $maxRetries = 3;
+ $hadLogContent = false;
$retryCount = 0;
- $maxPollAttempts = 300; // 10 minutes at 2-second intervals
$pollAttempts = 0;
- while ($isActive && $pollAttempts < $maxPollAttempts) {
+ while ($pollAttempts < self::MAX_POLL_ATTEMPTS) {
try {
- $response = $this->getDeploymentLog($serverId, $siteId, $deploymentId);
-
- if ($response && isset($response['content'])) {
- $newLines = $this->extractNewLines($response['content'], $lastLogPosition);
-
- if (! empty($newLines)) {
- foreach ($newLines as $line) {
- if (trim($line) !== '') { // Skip empty lines
- if ($onNewLines) {
- $onNewLines($line);
- } else {
- echo $line.PHP_EOL;
- }
- }
- }
-
- $lastLogPosition += count($newLines);
- }
+ $site = $this->ploi->getSiteDetails($serverId, $siteId)['data'] ?? null;
+
+ if (! $site) {
+ throw new Exception('Failed to fetch site details.');
}
- // Check deployment status to see if we should stop polling
- $deployment = $this->getDeployment($serverId, $siteId, $deploymentId);
- if ($deployment && ! in_array($deployment['status'], ['pending', 'running', 'deploying'])) {
- $isActive = false;
+ $currentLog = $site['current_deploy_log'] ?? null;
+ $status = $site['status'] ?? null;
- // Show final status
- if ($onNewLines) {
- $onNewLines("Deployment {$deployment['status']}");
- }
+ if ($currentLog !== null) {
+ $hadLogContent = true;
+ $lastLogPosition += $this->emitNewLines($currentLog, $lastLogPosition, $onNewLines);
}
- if ($isActive) {
- sleep($pollInterval);
- $pollAttempts++;
+ // Deployment finished if the API reports a terminal status, or if it cleared
+ // the log buffer after we had previously seen content.
+ $finished = in_array($status, self::FINAL_STATUSES, true)
+ || ($hadLogContent && $currentLog === null);
+
+ if ($finished) {
+ // The live `current_deploy_log` buffer is cleared the moment a deployment
+ // finishes, so its final lines never get polled. Emit anything we missed
+ // from the persisted deploy log before returning.
+ $lastLogPosition += $this->flushPersistedLog($serverId, $siteId, $lastLogPosition, $onNewLines);
+
+ return $status;
}
- // Reset retry count on successful poll
+ sleep(self::POLL_INTERVAL);
+ $pollAttempts++;
$retryCount = 0;
-
} catch (Exception $e) {
$retryCount++;
- if ($retryCount >= $maxRetries) {
+ if ($retryCount >= self::MAX_RETRIES) {
throw new Exception('Max retries reached. Last error: '.$e->getMessage());
}
+ $message = "Error polling logs (retry $retryCount/".self::MAX_RETRIES.'): '.$e->getMessage();
+
if ($onNewLines) {
- $onNewLines("Error polling logs (retry {$retryCount}/{$maxRetries}): ".$e->getMessage());
+ $onNewLines($message);
} else {
- echo "Error polling logs (retry {$retryCount}/{$maxRetries}): ".$e->getMessage().PHP_EOL;
+ echo $message.PHP_EOL;
}
- sleep(5); // Wait longer on error
- }
- }
-
- if ($pollAttempts >= $maxPollAttempts) {
- $message = 'Log polling timeout reached (10 minutes). Deployment may still be in progress.';
- if ($onNewLines) {
- $onNewLines($message);
- } else {
- echo $message.PHP_EOL;
+ sleep(self::RETRY_BACKOFF);
}
}
- }
- /**
- * Extract new lines from log content based on last position
- */
- private function extractNewLines(string $content, int $lastPosition): array
- {
- $lines = explode("\n", $content);
+ $timeoutMessage = 'Log polling timeout reached (10 minutes). Deployment may still be in progress.';
- if ($lastPosition < count($lines)) {
- return array_slice($lines, $lastPosition);
+ if ($onNewLines) {
+ $onNewLines($timeoutMessage);
+ } else {
+ echo $timeoutMessage.PHP_EOL;
}
- return [];
- }
-
- /**
- * Get deployment log content
- *
- * @throws Exception
- */
- private function getDeploymentLog(int $serverId, int $siteId, int $deploymentId): ?array
- {
- $response = $this->makeApiRequest("servers/{$serverId}/sites/{$siteId}/deployments/{$deploymentId}/log");
-
- return $response['data'] ?? null;
+ return null;
}
/**
- * Get deployment status
- *
- * @throws Exception
+ * Emit every non-empty line past `$lastPosition` and return how many lines were consumed.
*/
- private function getDeployment(int $serverId, int $siteId, int $deploymentId): ?array
+ private function emitNewLines(string $content, int $lastPosition, ?callable $onNewLines): int
{
- $response = $this->makeApiRequest("servers/{$serverId}/sites/{$siteId}/deployments");
+ $newLines = $this->extractNewLines($content, $lastPosition);
- $deployments = $response['data'] ?? [];
+ foreach ($newLines as $line) {
+ if (trim($line) === '') {
+ continue;
+ }
- foreach ($deployments as $deployment) {
- if ($deployment['id'] == $deploymentId) {
- return $deployment;
+ if ($onNewLines) {
+ $onNewLines($line);
+ } else {
+ echo $line.PHP_EOL;
}
}
- return null;
+ return count($newLines);
}
/**
- * Make authenticated API request
+ * Fetch the persisted deploy log and emit any lines the live buffer never streamed.
*
- * @throws Exception
+ * Best-effort: a failure here does not affect the already-reported deployment status.
*/
- private function makeApiRequest(string $endpoint): array
+ private function flushPersistedLog(int $serverId, int $siteId, int $lastPosition, ?callable $onNewLines): int
{
- $response = Http::withHeaders([
- 'Authorization' => 'Bearer '.$this->apiKey,
- 'Accept' => 'application/json',
- 'User-Agent' => 'Ploi CLI',
- ])->get($this->baseUrl.'/'.$endpoint);
-
- if (! $response->successful()) {
- throw new Exception('API request failed: '.$response->body());
- }
+ try {
+ $logs = $this->ploi->getSiteLogs($serverId, $siteId, 1)['data'] ?? [];
- return $response->json();
- }
+ if (empty($logs)) {
+ return 0;
+ }
- /**
- * Get the latest deployment for a site
- *
- * @throws Exception
- */
- public function getLatestDeployment(int $serverId, int $siteId): ?array
- {
- $response = $this->makeApiRequest("servers/{$serverId}/sites/{$siteId}/deployments?per_page=1");
+ $content = $this->ploi->getSiteLog($serverId, $siteId, $logs[0]['id'])['data']['content'] ?? null;
- $deployments = $response['data'] ?? [];
+ if ($content === null) {
+ return 0;
+ }
- return $deployments[0] ?? null;
+ return $this->emitNewLines($content, $lastPosition, $onNewLines);
+ } catch (Exception $e) {
+ return 0;
+ }
}
/**
- * Check if there's an active deployment
+ * Slice lines past the last reported position.
*
- * @throws Exception
+ * @return array
*/
- public function getActiveDeployment(int $serverId, int $siteId): ?array
+ private function extractNewLines(string $content, int $lastPosition): array
{
- $response = $this->makeApiRequest("servers/{$serverId}/sites/{$siteId}/deployments");
-
- $deployments = $response['data'] ?? [];
+ $lines = explode("\n", $content);
- foreach ($deployments as $deployment) {
- if (in_array($deployment['status'], ['pending', 'running', 'deploying'])) {
- return $deployment;
- }
+ if ($lastPosition < count($lines)) {
+ return array_slice($lines, $lastPosition);
}
- return null;
- }
-
- /**
- * Get all deployments for a site
- *
- * @throws Exception
- */
- public function getDeployments(int $serverId, int $siteId): array
- {
- $response = $this->makeApiRequest("servers/{$serverId}/sites/{$siteId}/deployments");
-
- return $response['data'] ?? [];
+ return [];
}
}