Integrate Coolify API client and module for managing Coolify services, enhancing automation and deployment processes.
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
<?php
|
||||
|
||||
app_require('classes/release_manager.php');
|
||||
app_require('classes/release_manager_schema_bootstrap.php');
|
||||
|
||||
use classes\release_manager;
|
||||
|
||||
it('redacts sensitive release timeline payload fields recursively', function (): void {
|
||||
$payload = [
|
||||
'Authorization' => 'Bearer secret-token',
|
||||
'nested' => [
|
||||
'api_key' => 'key-value',
|
||||
'safe' => 'visible',
|
||||
'items' => [
|
||||
['password' => 'hidden', 'status' => 500],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
expect(release_manager::redactPayload($payload))->toBe([
|
||||
'Authorization' => '[redacted]',
|
||||
'nested' => [
|
||||
'api_key' => '[redacted]',
|
||||
'safe' => 'visible',
|
||||
'items' => [
|
||||
['password' => '[redacted]', 'status' => 500],
|
||||
],
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it('verifies GitHub sha256 webhook signatures', function (): void {
|
||||
$secret = 'release-webhook-secret';
|
||||
$payload = '{"ref":"refs/heads/main"}';
|
||||
$signature = 'sha256=' . hash_hmac('sha256', $payload, $secret);
|
||||
|
||||
expect(release_manager::verifyGithubSignature($secret, $payload, $signature))->toBeTrue();
|
||||
expect(release_manager::verifyGithubSignature($secret, $payload, 'sha256=bad'))->toBeFalse();
|
||||
expect(release_manager::verifyGithubSignature('', $payload, $signature))->toBeFalse();
|
||||
});
|
||||
|
||||
it('normalizes GitHub repository identifiers for private repository access checks', function (): void {
|
||||
expect(release_manager::normalizeGithubRepositoryName('truckwash/backend-php'))->toBe('truckwash/backend-php');
|
||||
expect(release_manager::normalizeGithubRepositoryName('https://github.com/truckwash/front-end-vue.git'))->toBe('truckwash/front-end-vue');
|
||||
expect(release_manager::normalizeGithubRepositoryName('git@github.com:truckwash/backend-php.git'))->toBe('truckwash/backend-php');
|
||||
expect(release_manager::normalizeGithubRepositoryName('not a repository'))->toBe('');
|
||||
});
|
||||
|
||||
it('summarizes failed deployments and blocks promotion until a deployment succeeds', function (): void {
|
||||
$summary = release_manager::deploymentFailureSummary(
|
||||
new RuntimeException('Coolify API request failed: HTTP 404'),
|
||||
[
|
||||
'app' => 'api',
|
||||
'repository' => 'copenhagentruckwash/api',
|
||||
'branch' => 'master',
|
||||
'commit_sha' => 'ab31cd6dbb288606a24fcdbcb7bb7d58d843e4f3',
|
||||
'coolify_service_uuid' => 'api-service',
|
||||
]
|
||||
);
|
||||
|
||||
expect($summary['category'])->toBe('coolify_target');
|
||||
expect($summary['stage'])->toBe('provider_target');
|
||||
expect($summary['promotion_blocked'])->toBeTrue();
|
||||
expect($summary['evidence']['commit_sha'])->toBe('ab31cd6dbb288606a24fcdbcb7bb7d58d843e4f3');
|
||||
expect(release_manager::deploymentCanBePromoted('deployed'))->toBeTrue();
|
||||
expect(release_manager::deploymentCanBePromoted('failed'))->toBeFalse();
|
||||
|
||||
$reason = release_manager::deploymentPromotionBlockedReason([
|
||||
'status' => 'failed',
|
||||
'result_json' => json_encode(['failure_summary' => $summary]),
|
||||
]);
|
||||
|
||||
expect($reason)->toContain('Deployment failed');
|
||||
expect($reason)->toContain('HTTP 404');
|
||||
});
|
||||
|
||||
it('uses the selected Coolify project and resolves server UUID from the instance default', function (): void {
|
||||
$manager = new release_manager();
|
||||
$method = new ReflectionMethod(release_manager::class, 'releaseCoolifyServicePayload');
|
||||
$method->setAccessible(true);
|
||||
|
||||
$payload = $method->invoke($manager, [
|
||||
'channel_slug' => 'internal',
|
||||
'app' => 'api',
|
||||
'repository' => 'copenhagentruckwash/api',
|
||||
'branch' => 'master',
|
||||
], [
|
||||
'coolify_project_uuid' => 'project-selected',
|
||||
'coolify_deploy_now' => true,
|
||||
], [
|
||||
'default_project_uuid' => 'project-default',
|
||||
'default_environment_name' => 'production',
|
||||
'default_server_uuid' => 'server-default',
|
||||
]);
|
||||
|
||||
expect($payload['project_uuid'])->toBe('project-selected');
|
||||
expect($payload['server_uuid'])->toBe('server-default');
|
||||
expect($payload)->not->toHaveKey('coolify_server_uuid');
|
||||
});
|
||||
|
||||
it('supports isolated stack mode and names new Coolify services explicitly', function (): void {
|
||||
$manager = new release_manager();
|
||||
|
||||
$normalizeMode = new ReflectionMethod(release_manager::class, 'normalizeServiceSetMode');
|
||||
$normalizeMode->setAccessible(true);
|
||||
expect($normalizeMode->invoke($manager, ' isolated_stack '))->toBe('isolated_stack');
|
||||
|
||||
$payloadMethod = new ReflectionMethod(release_manager::class, 'releaseCoolifyServicePayload');
|
||||
$payloadMethod->setAccessible(true);
|
||||
$payload = $payloadMethod->invoke($manager, [
|
||||
'channel_slug' => 'internal',
|
||||
'app' => 'frontend',
|
||||
'repository' => 'copenhagentruckwash/front-end-vue',
|
||||
'branch' => 'main',
|
||||
], [
|
||||
'coolify_service_name' => 'release-internal-safe-stack-frontend',
|
||||
'coolify_project_uuid' => 'project-internal',
|
||||
'coolify_deploy_now' => true,
|
||||
], [
|
||||
'default_environment_name' => 'production',
|
||||
'default_server_uuid' => 'server-default',
|
||||
]);
|
||||
|
||||
expect($payload['name'])->toBe('release-internal-safe-stack-frontend');
|
||||
expect($payload['project_uuid'])->toBe('project-internal');
|
||||
});
|
||||
|
||||
it('defines release manager schema, routes, permissions, and system-status integration hooks', function (): void {
|
||||
$schema = file_get_contents(app_path('classes/release_manager_schema_bootstrap.php'));
|
||||
$manager = file_get_contents(app_path('classes/release_manager.php'));
|
||||
$route = file_get_contents(app_path('routes/releaseManagerRoute.php'));
|
||||
$auth = file_get_contents(app_path('routes/authRoute.php'));
|
||||
$response = file_get_contents(app_path('classes/response.php'));
|
||||
$status = file_get_contents(app_path('classes/superuser_system_status_service.php'));
|
||||
$index = file_get_contents(app_path('index.php'));
|
||||
|
||||
expect($schema)->toContain('CREATE TABLE IF NOT EXISTS release_channels');
|
||||
expect($schema)->toContain('CREATE TABLE IF NOT EXISTS release_versions');
|
||||
expect($schema)->toContain('CREATE TABLE IF NOT EXISTS release_channel_versions');
|
||||
expect($schema)->toContain('CREATE TABLE IF NOT EXISTS release_assignments');
|
||||
expect($schema)->toContain('CREATE TABLE IF NOT EXISTS release_deployment_targets');
|
||||
expect($schema)->toContain('CREATE TABLE IF NOT EXISTS release_service_sets');
|
||||
expect($schema)->toContain('CREATE TABLE IF NOT EXISTS release_deployments');
|
||||
expect($schema)->toContain('CREATE TABLE IF NOT EXISTS release_bundles');
|
||||
expect($schema)->toContain('CREATE TABLE IF NOT EXISTS release_replay_targets');
|
||||
expect($schema)->toContain('CREATE TABLE IF NOT EXISTS release_timeline_sessions');
|
||||
expect($schema)->toContain('CREATE TABLE IF NOT EXISTS release_timeline_events');
|
||||
expect($schema)->toContain('CREATE TABLE IF NOT EXISTS release_module_health_snapshots');
|
||||
expect($schema)->toContain("ensureColumn('release_channel_versions', 'service_set_id'");
|
||||
expect($schema)->toContain("ensureColumn('release_channel_versions', 'bundle_id'");
|
||||
expect($schema)->toContain("ensureColumn('release_deployments', 'deployment_kind'");
|
||||
expect($schema)->toContain("ensureModuleConfigDefault('ReleaseManager', 'enabled'");
|
||||
expect($schema)->toContain("ensureModuleConfigDefault('ReleaseManager', 'github_token'");
|
||||
expect($schema)->toContain("ensureModuleConfigDefault('ReleaseManager', 'github_api_url'");
|
||||
expect($schema)->toContain("['stable', 'Stable'");
|
||||
expect($schema)->toContain("['canary', 'Canary'");
|
||||
expect($schema)->toContain("['beta', 'Beta'");
|
||||
expect($schema)->toContain("['internal', 'Internal'");
|
||||
|
||||
expect($route)->toContain('/release/bootstrap');
|
||||
expect($route)->toContain('/release/runtime');
|
||||
expect($route)->toContain('/release/timeline/events');
|
||||
expect($route)->toContain('/release/github/webhook');
|
||||
expect($route)->toContain('/superuser/releases/config');
|
||||
expect($route)->toContain('/superuser/releases/github/repositories');
|
||||
expect($route)->toContain('/superuser/releases/github/branches');
|
||||
expect($route)->toContain('/superuser/releases/github/commits');
|
||||
expect($route)->toContain('/superuser/releases/github/test');
|
||||
expect($route)->toContain('/superuser/releases/channels');
|
||||
expect($route)->toContain('/superuser/releases/assignments');
|
||||
expect($route)->toContain('/superuser/releases/service-sets');
|
||||
expect($route)->toContain('/superuser/releases/bundles');
|
||||
expect($route)->toContain('/superuser/releases/bundles/{id}/deploy');
|
||||
expect($route)->toContain('/superuser/releases/bundles/{id}/promote');
|
||||
expect($route)->toContain('/superuser/releases/deployments');
|
||||
expect($route)->toContain('/superuser/releases/replay-targets');
|
||||
expect($route)->toContain('/superuser/releases/timeline');
|
||||
expect($route)->toContain("requirePermission('superuser_release_manager_view')");
|
||||
expect($route)->toContain("requirePermission('superuser_release_manager_manage')");
|
||||
expect($route)->toContain("requirePermission('superuser_release_manager_deploy')");
|
||||
expect($route)->toContain("requirePermission('superuser_release_manager_rollback')");
|
||||
expect($route)->toContain("requirePermission('superuser_release_manager_replay')");
|
||||
|
||||
expect($manager)->toContain('verifyGithubSignature');
|
||||
expect($manager)->toContain('normalizeGithubRepositoryName');
|
||||
expect($manager)->toContain('releaseConfig');
|
||||
expect($manager)->toContain('updateReleaseConfig');
|
||||
expect($manager)->toContain('github_token_variable');
|
||||
expect($manager)->toContain('github_token_env_variable');
|
||||
expect($manager)->toContain('listGithubRepositories');
|
||||
expect($manager)->toContain('listGithubBranches');
|
||||
expect($manager)->toContain('listGithubCommits');
|
||||
expect($manager)->toContain('testGithubRepositoryAccess');
|
||||
expect($manager)->toContain('githubRepositoryAccess');
|
||||
expect($manager)->toContain('github_token');
|
||||
expect($manager)->toContain('commit_mode');
|
||||
expect($manager)->toContain('restartCoolifyService');
|
||||
expect($manager)->toContain('deployCoolifyReleaseTarget');
|
||||
expect($manager)->toContain('listServiceSets');
|
||||
expect($manager)->toContain('createServiceSet');
|
||||
expect($manager)->toContain('createBundle');
|
||||
expect($manager)->toContain('deployBundle');
|
||||
expect($manager)->toContain('promoteBundle');
|
||||
expect($manager)->toContain('clone_replica_from_source');
|
||||
expect($manager)->toContain('register_isolated_empty_service');
|
||||
expect($manager)->toContain('isolated_stack');
|
||||
expect($manager)->toContain('create_isolated_empty_stack_service');
|
||||
expect($manager)->toContain('assertIsolatedStackTarget');
|
||||
expect($manager)->toContain('must not point at an existing Coolify service');
|
||||
expect($manager)->toContain("'data_promotion' => false");
|
||||
expect($manager)->toContain("'replica_failover' => false");
|
||||
expect($manager)->toContain('deploymentCanBePromoted');
|
||||
expect($manager)->toContain('deploymentFailureSummary');
|
||||
expect($manager)->toContain('coolifyProjectSuggestions');
|
||||
expect($manager)->toContain('releaseCoolifyServerUuid');
|
||||
expect($manager)->toContain('coolify_project_uuid');
|
||||
expect($manager)->toContain('releaseCoolifyServicePayload');
|
||||
expect($manager)->toContain('release_deployment_targets');
|
||||
expect($manager)->toContain('resolveChannel');
|
||||
expect($manager)->toContain('capturePolicyFor');
|
||||
expect($manager)->toContain('channelAvailability');
|
||||
expect($manager)->toContain("'availability' =>");
|
||||
expect($manager)->toContain('redactPayload');
|
||||
expect($manager)->toContain('moduleKeys');
|
||||
expect($manager)->toContain('releaseSuggestions');
|
||||
expect($manager)->toContain('load_balancer_domains');
|
||||
expect($manager)->toContain('appendDomainSuggestion');
|
||||
expect($manager)->toContain('Coolify SSL requires a DNS domain routed to the load balancer.');
|
||||
expect($manager)->toContain('coolify_services');
|
||||
expect($manager)->toContain('coolify_enable_ssl');
|
||||
expect($manager)->toContain('createService');
|
||||
expect($manager)->toContain('updateService');
|
||||
expect($manager)->toContain('channel_presets');
|
||||
expect($manager)->toContain('target_presets');
|
||||
|
||||
expect($auth)->toContain("'release' => (new release_manager())->runtimeForPayload");
|
||||
expect($response)->toContain('recordBackendFailure');
|
||||
expect($status)->toContain("'key' => 'releasemanager'");
|
||||
expect($status)->toContain('probeReleaseManagerModule');
|
||||
expect($index)->toContain('release_manager::initializeRequestContext');
|
||||
expect($index)->toContain('X-Release-Trace');
|
||||
expect($manager)->not->toContain('X-Release-Channel');
|
||||
});
|
||||
Reference in New Issue
Block a user