create(); $application = Application::factory()->for($organisation)->create(['name' => 'Billing API']); $environment = Environment::factory()->for($application)->create(); Service::factory()->for($environment)->create([ 'organisation_id' => $organisation->id, '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, 'desired_replicas' => 1, ]); $artifact = app(PlanBuildArtifact::class)->execute($environment, str_repeat('a', 40)); expect($artifact->status)->toBe(BuildArtifactStatus::PENDING) ->and($artifact->image_tag)->toBe('billing-api:aaaaaaaaaaaa') ->and($artifact->registry_ref)->toBeNull() ->and($artifact->metadata['build_strategy'])->toBe(BuildStrategy::TARGET_SERVER->value); }); it('requires a registry before planning multi-server builds', function () { config(['keystone.managed_registry.url' => null]); $organisation = Organisation::factory()->create(); $application = Application::factory()->for($organisation)->create(); $environment = Environment::factory()->for($application)->create(); Service::factory()->for($environment)->create([ 'organisation_id' => $organisation->id, '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, 'desired_replicas' => 2, ]); expect(fn () => app(PlanBuildArtifact::class)->execute($environment, str_repeat('b', 40))) ->toThrow(RuntimeException::class, 'A registry is required before building artifacts for multi-server deployments.'); }); it('plans multi-server builds against a persisted managed registry', function () { $organisation = Organisation::factory()->create(); $server = buildEnabledServerFor($organisation); $organisation->registries()->create([ 'name' => 'Managed', 'type' => RegistryType::MANAGED, 'url' => 'registry.example.com', 'credentials' => [ 'build_username' => 'keystone-build', 'build_password' => 'secret', 'runtime_username' => 'keystone-runtime', 'runtime_password' => 'runtime-secret', ], 'control_server_id' => $server->id, 'health_status' => 'healthy', 'readiness_checks' => ['control_https' => 'passed', 'build_push' => 'passed'], 'ready_at' => now(), ]); $application = Application::factory()->for($organisation)->create(['name' => 'Billing API']); $environment = Environment::factory()->for($application)->create(); Service::factory()->for($environment)->create([ 'organisation_id' => $organisation->id, '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, 'desired_replicas' => 2, ]); $artifact = app(PlanBuildArtifact::class)->execute($environment, str_repeat('d', 40)); expect($artifact->image_tag)->toBe("keystone/{$application->uuid}/{$environment->uuid}:dddddddddddd") ->and($artifact->registry_ref)->toBe("registry.example.com/keystone/{$application->uuid}/{$environment->uuid}:dddddddddddd") ->and($artifact->metadata['build_strategy'])->toBe(BuildStrategy::DEDICATED_BUILDER->value) ->and($artifact->metadata['build_server_id'])->toBe($server->id) ->and($artifact->metadata['registry_type'])->toBe(RegistryType::MANAGED->value); }); it('blocks managed registry builds until the registry is ready', function () { $organisation = Organisation::factory()->create(); $server = buildEnabledServerFor($organisation); $organisation->registries()->create([ 'name' => 'Managed', 'type' => RegistryType::MANAGED, 'url' => 'registry.example.com', 'credentials' => [ 'build_username' => 'keystone-build', 'build_password' => 'secret', 'runtime_username' => 'keystone-runtime', 'runtime_password' => 'runtime-secret', ], 'control_server_id' => $server->id, 'health_status' => 'pending', ]); $application = Application::factory()->for($organisation)->create(); $environment = Environment::factory()->for($application)->create(); Service::factory()->for($environment)->create([ 'organisation_id' => $organisation->id, '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, 'desired_replicas' => 2, ]); expect(fn () => app(PlanBuildArtifact::class)->execute($environment, str_repeat('g', 40))) ->toThrow(RuntimeException::class, 'Managed registry has not passed readiness checks.'); }); it('does not treat the managed registry config value as a ready registry record', function () { config(['keystone.managed_registry.url' => 'registry.example.com']); $organisation = Organisation::factory()->create(); $application = Application::factory()->for($organisation)->create(); $environment = Environment::factory()->for($application)->create(); Service::factory()->for($environment)->create([ 'organisation_id' => $organisation->id, '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, 'desired_replicas' => 2, ]); expect(fn () => app(PlanBuildArtifact::class)->execute($environment, str_repeat('e', 40))) ->toThrow(RuntimeException::class, 'A registry is required before building artifacts for multi-server deployments.'); }); it('plans multi-server builds against the configured external registry', function () { $organisation = Organisation::factory()->create(); $organisation->registries()->create([ 'name' => 'GHCR', 'type' => RegistryType::GHCR, 'url' => 'ghcr.io/example', ]); $application = Application::factory()->for($organisation)->create(['name' => 'Billing API']); $environment = Environment::factory()->for($application)->create(); Service::factory()->for($environment)->create([ 'organisation_id' => $organisation->id, '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, 'desired_replicas' => 2, ]); $artifact = app(PlanBuildArtifact::class)->execute($environment, str_repeat('c', 40)); expect($artifact->registry_ref)->toBe('ghcr.io/example/billing-api:cccccccccccc') ->and($artifact->metadata['build_strategy'])->toBe(BuildStrategy::EXTERNAL_REGISTRY->value); }); it('prefers a configured external registry over the managed default', function () { $organisation = Organisation::factory()->create(); $server = buildEnabledServerFor($organisation); $organisation->registries()->create([ 'name' => 'Managed', 'type' => RegistryType::MANAGED, 'url' => 'registry.example.com', 'credentials' => [ 'build_username' => 'keystone-build', 'build_password' => 'secret', 'runtime_username' => 'keystone-runtime', 'runtime_password' => 'runtime-secret', ], 'control_server_id' => $server->id, 'health_status' => 'healthy', 'readiness_checks' => ['control_https' => 'passed', 'build_push' => 'passed'], 'ready_at' => now(), ]); $organisation->registries()->create([ 'name' => 'GHCR', 'type' => RegistryType::GHCR, 'url' => 'ghcr.io/example', ]); $application = Application::factory()->for($organisation)->create(['name' => 'Billing API']); $environment = Environment::factory()->for($application)->create(); Service::factory()->for($environment)->create([ 'organisation_id' => $organisation->id, '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, 'desired_replicas' => 2, ]); $artifact = app(PlanBuildArtifact::class)->execute($environment, str_repeat('f', 40)); expect($artifact->registry_ref)->toBe('ghcr.io/example/billing-api:ffffffffffff') ->and($artifact->metadata['build_strategy'])->toBe(BuildStrategy::EXTERNAL_REGISTRY->value) ->and($artifact->metadata['registry_type'])->toBe(RegistryType::GHCR->value); }); function buildEnabledServerFor(Organisation $organisation): Server { $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', ]); return Server::factory() ->forOrganisation($organisation->id) ->forProvider($provider->id) ->forNetwork($network->id) ->create([ 'is_control_node' => true, 'build_enabled' => true, ]); }