Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/Facades/SSH.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
* @method static string write(string $path, string $content, string $owner = null)
* @method static string assertExecuted(mixed $commands)
* @method static string assertExecutedContains(string $command)
* @method static string assertNotExecutedContains(string $command, string $message = '')
* @method static string assertFileUploaded(string $toPath, ?string $content = null)
* @method static string getUploadedLocalPath()
* @method static disconnect()
Expand Down
6 changes: 4 additions & 2 deletions app/Jobs/Site/DeployJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ private function handleClassicDeployment($site, $log): void
if ($site->deploymentScript->shouldRestartWorkers()) {
/** @var ProcessManager $processManager */
$processManager = $site->server->processManager()->handler();
$processManager->restartAll($site->id);
$workerIds = $site->workers()->pluck('id')->toArray();
$processManager->restartByIds($workerIds, $site->id);
}
}

Expand Down Expand Up @@ -132,7 +133,8 @@ private function handleModernDeployment($site, $log): void
if ($site->preFlightScript?->shouldRestartWorkers()) {
/** @var ProcessManager $processManager */
$processManager = $site->server->processManager()->handler();
$processManager->restartAll($site->id);
$workerIds = $site->workers()->pluck('id')->toArray();
$processManager->restartByIds($workerIds, $site->id);
}
}
}
5 changes: 5 additions & 0 deletions app/Services/ProcessManager/ProcessManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,10 @@ public function start(int $id, ?int $siteId = null): void;

public function restartAll(?int $siteId = null): void;

/**
* @param array<int> $workerIds
*/
public function restartByIds(array $workerIds, ?int $siteId = null): void;

public function getLogs(string $user, string $logPath): string;
}
20 changes: 20 additions & 0 deletions app/Services/ProcessManager/Supervisor.php
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,26 @@ public function restartAll(?int $siteId = null): void
);
}

/**
* @param array<int> $workerIds
*
* @throws Throwable
*/
public function restartByIds(array $workerIds, ?int $siteId = null): void
{
if (empty($workerIds)) {
return;
}

$this->service->server->ssh()->exec(
view('ssh.services.process-manager.supervisor.restart-workers', [
'workerIds' => $workerIds,
]),
'restart-workers',
$siteId
);
}

/**
* @throws Throwable
*/
Expand Down
12 changes: 12 additions & 0 deletions app/Support/Testing/SSHFake.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,18 @@ public function assertExecutedContains(string $command): void
);
}

public function assertNotExecutedContains(string $command, string $message = ''): void
{
foreach ($this->commands as $executedCommand) {
$commandStr = (string) $executedCommand;
if (str($commandStr)->contains($command)) {
Assert::fail(
$message ?: "The command '{$command}' should not be executed, but it was found in: {$commandStr}"
);
}
}
}

public function assertFileUploaded(string $toPath, ?string $content = null): void
{
if ($this->uploadedLocalPath === '' || $this->uploadedLocalPath === '0' || ($this->uploadedRemotePath === '' || $this->uploadedRemotePath === '0')) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
@foreach($workerIds as $workerId)
if ! sudo supervisorctl restart {{ $workerId }}:*; then
echo 'VITO_SSH_ERROR' && exit 1
fi
@endforeach

echo "Workers restarted successfully."
134 changes: 134 additions & 0 deletions tests/Feature/ApplicationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
namespace Tests\Feature;

use App\Enums\DeploymentStatus;
use App\Enums\WorkerStatus;
use App\Facades\SSH;
use App\Models\Deployment;
use App\Models\GitHook;
use App\Models\Site;
use App\Models\Worker;
use App\Notifications\DeploymentCompleted;
use Exception;
use Illuminate\Foundation\Testing\RefreshDatabase;
Expand Down Expand Up @@ -557,4 +560,135 @@ public static function hookData(): array
],
];
}

public function test_deploy_classic_restarts_only_site_workers(): void
{
$sshFake = SSH::fake('fake output');
Http::fake([
'github.com/*' => Http::response([
'sha' => '123',
'commit' => [
'message' => 'test commit message',
'name' => 'test commit name',
'email' => 'test@example.com',
'url' => 'https://github.com/commit-url',
],
]),
]);
Notification::fake();

// Create a worker for the site being deployed
$siteWorker = Worker::factory()->create([
'server_id' => $this->server->id,
'site_id' => $this->site->id,
'status' => WorkerStatus::RUNNING,
]);

// Create another site with workers on the same server
$otherSite = Site::factory()->create([
'server_id' => $this->server->id,
]);
$otherSiteWorker = Worker::factory()->create([
'server_id' => $this->server->id,
'site_id' => $otherSite->id,
'status' => WorkerStatus::RUNNING,
]);

// Enable restart workers for the deployment script
$this->site->deploymentScript->update([
'content' => 'git pull',
'configs' => ['restart_workers' => true],
]);

$this->actingAs($this->user);

$this->post(route('application.deploy', [
'server' => $this->server,
'site' => $this->site,
]))
->assertSessionDoesntHaveErrors();

// Verify that only the site worker restart command was executed
SSH::assertExecutedContains('supervisorctl restart '.$siteWorker->id.':*');

// Verify that other site's worker and "restart all" are not executed
$this->assertWorkerNotRestarted($otherSiteWorker->id);
SSH::assertNotExecutedContains('supervisorctl restart all', 'Should not restart all workers');
}

public function test_deploy_modern_restarts_only_site_workers(): void
{
$sshFake = SSH::fake('fake output');
Http::fake([
'github.com/*' => Http::response([
'sha' => '123',
'commit' => [
'message' => 'test commit message',
'name' => 'test commit name',
'email' => 'test@example.com',
'url' => 'https://github.com/commit-url',
],
]),
]);
Notification::fake();

$this->site->update([
'type_data' => [
'modern_deployment' => true,
'modern_deployment_history' => 10,
'modern_deployment_shared_resources' => ['.env'],
],
]);
$this->site->ensureDeploymentScriptsExist();
$this->site->refresh();

// Create a worker for the site being deployed
$siteWorker = Worker::factory()->create([
'server_id' => $this->server->id,
'site_id' => $this->site->id,
'status' => WorkerStatus::RUNNING,
]);

// Create another site with workers on the same server
$otherSite = Site::factory()->create([
'server_id' => $this->server->id,
]);
$otherSiteWorker = Worker::factory()->create([
'server_id' => $this->server->id,
'site_id' => $otherSite->id,
'status' => WorkerStatus::RUNNING,
]);

// Enable restart workers for the pre-flight script
$this->site->preFlightScript->update([
'content' => 'php artisan migrate --force',
'configs' => ['restart_workers' => true],
]);

$this->actingAs($this->user);

$this->post(route('application.deploy', [
'server' => $this->server,
'site' => $this->site,
]))
->assertSessionDoesntHaveErrors();

// Verify that only the site worker restart command was executed
SSH::assertExecutedContains('supervisorctl restart '.$siteWorker->id.':*');

// Verify that other site's worker and "restart all" are not executed
$this->assertWorkerNotRestarted($otherSiteWorker->id);
SSH::assertNotExecutedContains('supervisorctl restart all', 'Should not restart all workers');
}

/**
* Assert that the given worker's restart command was not executed via SSH.
*/
private function assertWorkerNotRestarted(int|string $workerId): void
{
SSH::assertNotExecutedContains(
'supervisorctl restart '.$workerId.':*',
"Worker {$workerId} should not be restarted"
);
}
}