create(); $organisation = Organisation::factory()->create(['owner_id' => $user->id]); $provider = Provider::factory()->forOrganisation($organisation)->create(); $network = Network::create([ 'organisation_id' => $organisation->id, 'provider_id' => $provider->id, 'name' => 'test-network', 'ip_range' => '10.0.0.0/24', ]); $server = Server::factory() ->forOrganisation($organisation->id) ->forProvider($provider->id) ->forNetwork($network->id) ->create(); $application = Application::factory()->for($organisation)->create(); app(GenerateDeployKey::class)->execute($application, [ 'public' => 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAITestPublicKey keystone', 'private' => "-----BEGIN OPENSSH PRIVATE KEY-----\ntest\n-----END OPENSSH PRIVATE KEY-----", 'fingerprint' => 'SHA256:test', ]); $environment = Environment::factory()->for($application)->create(); $service = Service::factory()->for($environment)->for($server)->create([ 'organisation_id' => $organisation->id, 'name' => 'web', 'category' => ServiceCategory::APPLICATION, 'type' => ServiceType::LARAVEL, 'version' => 'php-8.4', 'version_track' => 'php-8.4', 'driver_name' => 'laravel.php-8.4', 'deploy_policy' => DeployPolicy::WITH_ENVIRONMENT, ]); app()->instance(RemoteCommandRunner::class, new class implements RemoteCommandRunner { public function run(Server $server, string $script): string { return "container_id=container-1\nhealth_status=running\nimage_digest=billing-api:aaaaaaaaaaaa@sha256:controllerdigest\n"; } }); Process::fake([ '*' => Process::result(output: str_repeat('a', 40)."\trefs/heads/main\n"), ]); $response = $this->actingAs($user)->post(route('environment-deployments.store', [ 'organisation' => $organisation->id, 'application' => $application->id, 'environment' => $environment->id, ])); $response->assertRedirect(route('environments.show', [ 'organisation' => $organisation->id, 'application' => $application->id, 'environment' => $environment->id, ])); $parent = Operation::query() ->whereMorphedTo('target', $environment) ->where('kind', OperationKind::ENVIRONMENT_DEPLOY) ->first(); $serviceDeploy = Operation::query() ->whereMorphedTo('target', $service) ->where('kind', OperationKind::SERVICE_DEPLOY) ->first(); expect($parent)->not->toBeNull() ->and($parent->status)->toBe(OperationStatus::COMPLETED) ->and($serviceDeploy)->not->toBeNull() ->and($serviceDeploy->status)->toBe(OperationStatus::COMPLETED) ->and($serviceDeploy->steps()->where('name', 'Render Compose files')->first()->script)->toContain("base64 -d > /home/keystone/services/{$service->id}/compose.yml") ->and($serviceDeploy->steps()->where('name', 'Render Compose files')->first()->script)->toContain("base64 -d > /home/keystone/services/{$service->id}/.env") ->and($service->refresh()->replicas)->toHaveCount(1) ->and($service->available_image_digest)->toBe('sha256:controllerdigest'); }); it('blocks multi-server environment deployment until a registry is configured', function () { $user = User::factory()->create(); $organisation = Organisation::factory()->create(['owner_id' => $user->id]); $provider = Provider::factory()->forOrganisation($organisation)->create(); $network = Network::create([ 'organisation_id' => $organisation->id, 'provider_id' => $provider->id, 'name' => 'test-network', 'ip_range' => '10.0.0.0/24', ]); $primaryServer = Server::factory() ->forOrganisation($organisation->id) ->forProvider($provider->id) ->forNetwork($network->id) ->create(); $secondaryServer = Server::factory() ->forOrganisation($organisation->id) ->forProvider($provider->id) ->forNetwork($network->id) ->create(); $application = Application::factory()->for($organisation)->create(); $environment = Environment::factory()->for($application)->create(); $service = Service::factory()->for($environment)->for($primaryServer)->create([ 'organisation_id' => $organisation->id, 'category' => ServiceCategory::APPLICATION, 'deploy_policy' => DeployPolicy::WITH_ENVIRONMENT, ]); ServiceReplica::factory() ->for($service) ->for($secondaryServer, 'server') ->create(); $this->actingAs($user) ->post(route('environment-deployments.store', [$organisation, $application, $environment])) ->assertRedirect() ->assertSessionHas('error', 'Configure a registry before deploying this environment to multiple servers.'); expect($environment->operations()->exists())->toBeFalse(); $organisation->registries()->create([ 'name' => 'GHCR', 'type' => RegistryType::GHCR, 'url' => 'ghcr.io/example', 'credentials' => ['username' => 'keystone', 'password' => 'secret'], ]); Bus::fake(); $this->actingAs($user) ->post(route('environment-deployments.store', [$organisation, $application, $environment])) ->assertRedirect(route('environments.show', [$organisation, $application, $environment])); Bus::assertDispatched(DeployEnvironment::class); }); it('deploys an environment at a specific commit when provided', function () { $user = User::factory()->create(); $organisation = Organisation::factory()->create(['owner_id' => $user->id]); $provider = Provider::factory()->forOrganisation($organisation)->create(); $network = Network::create([ 'organisation_id' => $organisation->id, 'provider_id' => $provider->id, 'name' => 'test-network', 'ip_range' => '10.0.0.0/24', ]); $server = Server::factory() ->forOrganisation($organisation->id) ->forProvider($provider->id) ->forNetwork($network->id) ->create(); $application = Application::factory()->for($organisation)->create(); app(GenerateDeployKey::class)->execute($application, [ 'public' => 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAITestPublicKey keystone', 'private' => "-----BEGIN OPENSSH PRIVATE KEY-----\ntest\n-----END OPENSSH PRIVATE KEY-----", 'fingerprint' => 'SHA256:test', ]); $environment = Environment::factory()->for($application)->create(); $service = Service::factory()->for($environment)->for($server)->create([ 'organisation_id' => $organisation->id, 'name' => 'web', 'category' => ServiceCategory::APPLICATION, 'type' => ServiceType::LARAVEL, 'version' => 'php-8.4', 'version_track' => 'php-8.4', 'driver_name' => 'laravel.php-8.4', 'deploy_policy' => DeployPolicy::WITH_ENVIRONMENT, ]); $targetCommit = str_repeat('b', 40); app()->instance(RemoteCommandRunner::class, new class implements RemoteCommandRunner { public function run(Server $server, string $script): string { return "container_id=container-1\nhealth_status=running\nimage_digest=billing-api:bbbbbbbbbbbb@sha256:manualcommit\n"; } }); Process::fake([ '*' => Process::result(output: str_repeat('a', 40)."\trefs/heads/main\n"), ]); $this->actingAs($user) ->post(route('environment-deployments.store', [ 'organisation' => $organisation->id, 'application' => $application->id, 'environment' => $environment->id, ]), [ 'target_commit' => $targetCommit, ]) ->assertRedirect(route('environments.show', [ 'organisation' => $organisation->id, 'application' => $application->id, 'environment' => $environment->id, ])); expect($service->refresh()->desired_revision)->toBe($targetCommit) ->and($environment->buildArtifacts()->where('commit_sha', $targetCommit)->exists())->toBeTrue(); }); it('validates the optional environment deployment commit', function () { $user = User::factory()->create(); $organisation = Organisation::factory()->create(['owner_id' => $user->id]); $application = Application::factory()->for($organisation)->create(); $environment = Environment::factory()->for($application)->create(); $this->actingAs($user) ->post(route('environment-deployments.store', [$organisation, $application, $environment]), [ 'target_commit' => 'not-a-sha', ]) ->assertInvalid(['target_commit']); });