Add tests for GitHub commit timestamp handling and runtime channel selection in ReleaseManager. Extend release URL normalization and runtime channel methods, and introduce assignment subject searches.

This commit is contained in:
Jeppe Bundgaard
2026-05-20 11:27:29 +02:00
parent c2263b8c98
commit 24ac681365
3 changed files with 743 additions and 33 deletions
@@ -46,6 +46,32 @@ it('normalizes GitHub repository identifiers for private repository access check
expect(release_manager::normalizeGithubRepositoryName('not a repository'))->toBe('');
});
it('keeps GitHub commit timestamps in public release manager commit payloads', function (): void {
$manager = new release_manager();
$method = new ReflectionMethod(release_manager::class, 'publicGithubCommit');
$method->setAccessible(true);
$commit = $method->invoke($manager, [
'sha' => 'feedface00000000000000000000000000000000',
'html_url' => 'https://github.com/truckwash/backend-php/commit/feedface',
'commit' => [
'message' => "Deploy release bundle\n\nBody is intentionally omitted from option labels.",
'author' => [
'name' => 'Release Bot',
'date' => '2026-05-19T08:10:00Z',
],
],
]);
expect($commit)->toMatchArray([
'sha' => 'feedface00000000000000000000000000000000',
'short_sha' => 'feedface0000',
'message' => 'Deploy release bundle',
'author_name' => 'Release Bot',
'authored_at' => '2026-05-19T08:10:00Z',
]);
});
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'),
@@ -381,6 +407,8 @@ it('defines release manager schema, routes, permissions, and system-status integ
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/channels/{id}/bundle');
expect($route)->toContain('/superuser/releases/assignment-subjects');
expect($route)->toContain('/superuser/releases/assignments');
expect($route)->toContain('/superuser/releases/service-sets');
expect($route)->toContain("\$this->delete('/superuser/releases/service-sets/{id}'");
@@ -424,6 +452,12 @@ it('defines release manager schema, routes, permissions, and system-status integ
expect($manager)->toContain('createBundle');
expect($manager)->toContain('deployBundle');
expect($manager)->toContain('promoteBundle');
expect($manager)->toContain('setChannelBundle');
expect($manager)->toContain('searchAssignmentSubjects');
expect($manager)->toContain('publicAssignmentSubjectSuggestion');
expect($manager)->toContain('available_channels');
expect($manager)->toContain('chooseRuntimeChannel');
expect($manager)->toContain('requestedRuntimeChannelSlug');
expect($manager)->toContain("status = 'superseded'");
expect($manager)->toContain('serviceSetIsActive');
expect($manager)->toContain('bundleIsActive');
@@ -469,6 +503,7 @@ it('defines release manager schema, routes, permissions, and system-status integ
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('releaseRuntimeUrls');
expect($manager)->toContain('coolify_services');
expect($manager)->toContain('coolify_enable_ssl');
expect($manager)->toContain('createService');
@@ -484,3 +519,145 @@ it('defines release manager schema, routes, permissions, and system-status integ
expect($index)->toContain('X-Release-Trace');
expect($manager)->not->toContain('X-Release-Channel');
});
it('requires non-default release channel runtime URLs and preserves load balancer paths', function (): void {
$manager = new release_manager();
$availability = new ReflectionMethod(release_manager::class, 'channelAvailability');
$availability->setAccessible(true);
$runtimeUrls = new ReflectionMethod(release_manager::class, 'releaseRuntimeUrls');
$runtimeUrls->setAccessible(true);
$publicUrl = new ReflectionMethod(release_manager::class, 'releaseCoolifyPublicUrl');
$publicUrl->setAccessible(true);
expect($availability->invoke($manager, [
'id' => 1,
'slug' => 'stable',
'default_channel' => 1,
'frontend_base_url' => null,
'api_base_url' => null,
]))->toMatchArray([
'configured' => true,
'missing' => [],
'status' => 'ready',
]);
expect($runtimeUrls->invoke($manager, [
'id' => 2,
'slug' => 'canary',
'default_channel' => 0,
'frontend_base_url' => null,
'api_base_url' => null,
], [
'frontend' => ['deployed_url' => 'https://api-v2.truckwash.io/canary/frontend/health'],
'api' => ['deployed_url' => 'https://api-v2.truckwash.io/canary/api/ping'],
]))->toBe([
'frontend_base_url' => 'https://api-v2.truckwash.io/canary/frontend',
'api_base_url' => 'https://api-v2.truckwash.io/canary/api',
]);
expect($publicUrl->invoke($manager, ['app' => 'frontend'], [
'coolify_public_url' => 'https://api-v2.truckwash.io/canary/frontend',
'coolify_enable_ssl' => true,
]))->toBe('https://api-v2.truckwash.io/canary/frontend');
$source = file(app_path('classes/release_manager.php'));
$methodSource = implode('', array_slice(
$source,
$availability->getStartLine() - 1,
$availability->getEndLine() - $availability->getStartLine() + 1
));
expect($methodSource)->toContain('frontend_base_url');
expect($methodSource)->toContain('api_base_url');
expect($methodSource)->toContain('release_bundle');
expect($methodSource)->toContain('frontend_version');
expect($methodSource)->toContain('api_version');
});
it('exposes release version git commit metadata for runtime channel cards', function (): void {
$manager = new release_manager();
$publicVersion = new ReflectionMethod(release_manager::class, 'publicVersion');
$publicVersion->setAccessible(true);
$version = $publicVersion->invoke($manager, [
'id' => 12,
'app' => 'frontend',
'repository' => 'truckwash/front-end-vue',
'branch' => 'release/canary',
'commit_sha' => 'c0ffee0000001111222233334444555566667777',
'tag' => null,
'version_label' => 'frontend-canary',
'build_url' => null,
'artifact_url' => null,
'deployed_url' => null,
'status' => 'active',
'metadata_json' => json_encode([
'github_access' => [
'commit' => [
'sha' => 'c0ffee0000001111222233334444555566667777',
'authored_at' => '2026-05-19T10:15:00Z',
],
],
]),
'created_at' => '2026-05-19 10:10:00',
'deployed_at' => '2026-05-19 10:20:00',
]);
expect($version['commit_sha'])->toBe('c0ffee0000001111222233334444555566667777');
expect($version['commit']['sha'])->toBe('c0ffee0000001111222233334444555566667777');
expect($version['commit_authored_at'])->toBe('2026-05-19T10:15:00Z');
expect($version['deployed_at'])->toBe('2026-05-19 10:20:00');
});
it('chooses a requested runtime channel only when it is available to the principal', function (): void {
$manager = new release_manager();
$choose = new ReflectionMethod(release_manager::class, 'chooseRuntimeChannel');
$choose->setAccessible(true);
$stable = [
'id' => 1,
'slug' => 'stable',
'name' => 'Stable',
'default_channel' => 1,
];
$canary = [
'id' => 2,
'slug' => 'canary',
'name' => 'Canary',
'default_channel' => 0,
];
expect($choose->invoke($manager, $stable, [$stable, $canary], 'canary'))->toBe($canary);
expect($choose->invoke($manager, $stable, [$stable, $canary], 'unknown'))->toBe($stable);
expect($choose->invoke($manager, $canary, [$stable, $canary], ''))->toBe($canary);
});
it('normalizes release assignment subject suggestions without leaking private fields', function (): void {
$suggestion = release_manager::publicAssignmentSubjectSuggestion([
'subject_type' => 'USER',
'subject_id' => 42,
'title' => ' Dispatcher ',
'description' => 'Customer #424242 / dispatcher@example.test',
'icon' => 'fas fa-user',
'source' => 'users',
'password' => 'secret',
'two_factor_secret' => 'private',
]);
expect($suggestion)->toBe([
'subject_type' => 'user',
'subject_id' => '42',
'label' => 'Dispatcher - Customer #424242 / dispatcher@example.test',
'title' => 'Dispatcher',
'description' => 'Customer #424242 / dispatcher@example.test',
'icon' => 'fas fa-user',
'source' => 'users',
]);
expect(array_keys($suggestion))->not->toContain('password');
expect(array_keys($suggestion))->not->toContain('two_factor_secret');
expect(release_manager::publicAssignmentSubjectSuggestion([
'subject_type' => 'invalid',
'subject_id' => 42,
'title' => 'Invalid',
]))->toBeNull();
});