620 lines
23 KiB
PHP
620 lines
23 KiB
PHP
<?php
|
|
|
|
app_require('classes/release_manager.php');
|
|
|
|
use classes\release_manager;
|
|
|
|
function releaseStatusOverviewForTest(array $summary): array
|
|
{
|
|
$manager = new release_manager();
|
|
$method = new ReflectionMethod(release_manager::class, 'releaseStatusOverview');
|
|
$method->setAccessible(true);
|
|
|
|
return $method->invoke($manager, array_replace([
|
|
'generated_at' => '2026-05-20T10:00:00+00:00',
|
|
'channels' => [],
|
|
'deployment_targets' => [],
|
|
'service_sets' => [],
|
|
'deployments' => [],
|
|
], $summary));
|
|
}
|
|
|
|
function releaseStatusChannelBySlug(array $overview, string $slug): array
|
|
{
|
|
foreach ($overview['channels'] as $channel) {
|
|
if (($channel['channel_slug'] ?? '') === $slug) {
|
|
return $channel;
|
|
}
|
|
}
|
|
|
|
throw new RuntimeException('Release status channel not found: ' . $slug);
|
|
}
|
|
|
|
function releaseStatusServiceByKey(array $channel, string $key): array
|
|
{
|
|
foreach ($channel['services'] as $service) {
|
|
if (($service['service_key'] ?? '') === $key) {
|
|
return $service;
|
|
}
|
|
}
|
|
|
|
throw new RuntimeException('Release status service not found: ' . $key);
|
|
}
|
|
|
|
function releaseStatusReadyService(string $kind, int $id): array
|
|
{
|
|
return [
|
|
'id' => $id,
|
|
'resource_uuid' => $kind . '-resource',
|
|
'resource_name' => ucfirst($kind),
|
|
'deployment_status' => 'deployed',
|
|
'availability_state' => 'failover_ready',
|
|
'replication' => [
|
|
'status' => 'ok',
|
|
'last_status' => [
|
|
'status' => 'ok',
|
|
'blockers' => [],
|
|
],
|
|
],
|
|
];
|
|
}
|
|
|
|
function releaseStatusReadyVersions(int $bundleId = 7): array
|
|
{
|
|
return [
|
|
'bundle_id' => $bundleId,
|
|
'bundle_label' => '#' . $bundleId,
|
|
'frontend' => [
|
|
'id' => 101,
|
|
'version_label' => 'frontend-2026-05-20',
|
|
'status' => 'active',
|
|
],
|
|
'api' => [
|
|
'id' => 102,
|
|
'version_label' => 'api-2026-05-20',
|
|
'status' => 'active',
|
|
],
|
|
];
|
|
}
|
|
|
|
it('marks a channel ready when all release services are healthy', function (): void {
|
|
$overview = releaseStatusOverviewForTest([
|
|
'channels' => [
|
|
[
|
|
'id' => 1,
|
|
'slug' => 'stable',
|
|
'name' => 'Stable',
|
|
'default_channel' => true,
|
|
'enabled' => true,
|
|
'frontend_base_url' => 'https://app.example.test',
|
|
'api_base_url' => 'https://api.example.test',
|
|
'versions' => releaseStatusReadyVersions(),
|
|
],
|
|
],
|
|
'deployment_targets' => [
|
|
['id' => 11, 'channel_id' => 1, 'app' => 'frontend', 'repository' => 'truckwash/front-end-vue'],
|
|
['id' => 12, 'channel_id' => 1, 'app' => 'api', 'repository' => 'truckwash/backend-php'],
|
|
],
|
|
'service_sets' => [
|
|
[
|
|
'id' => 21,
|
|
'channel_id' => 1,
|
|
'mode' => 'isolated_stack',
|
|
'stack' => [
|
|
'database' => releaseStatusReadyService('database', 31),
|
|
'redis' => releaseStatusReadyService('redis', 32),
|
|
'minio' => releaseStatusReadyService('minio', 33),
|
|
],
|
|
],
|
|
],
|
|
'deployments' => [
|
|
['id' => 41, 'channel_id' => 1, 'app' => 'frontend', 'status' => 'deployed'],
|
|
['id' => 42, 'channel_id' => 1, 'app' => 'api', 'status' => 'deployed'],
|
|
],
|
|
]);
|
|
|
|
$channel = releaseStatusChannelBySlug($overview, 'stable');
|
|
|
|
expect($overview['state'])->toBe('ready')
|
|
->and($overview['totals']['critical'])->toBe(0)
|
|
->and($overview['totals']['warning'])->toBe(0)
|
|
->and($overview['totals']['services'])->toBe(5)
|
|
->and($channel['readiness'])->toBe('ready')
|
|
->and($channel['services'])->toHaveCount(5);
|
|
});
|
|
|
|
it('does not block channels on unhealthy data targets when data services are production shared', function (): void {
|
|
$database = releaseStatusReadyService('database', 71);
|
|
$database['deployment_status'] = 'reconcile_failed';
|
|
$database['availability_state'] = 'degraded';
|
|
|
|
$overview = releaseStatusOverviewForTest([
|
|
'channels' => [
|
|
[
|
|
'id' => 2,
|
|
'slug' => 'beta',
|
|
'name' => 'Beta',
|
|
'default_channel' => false,
|
|
'enabled' => true,
|
|
'frontend_base_url' => 'https://beta.example.test',
|
|
'api_base_url' => 'https://api-beta.example.test',
|
|
'versions' => releaseStatusReadyVersions(8),
|
|
],
|
|
],
|
|
'deployment_targets' => [
|
|
['id' => 51, 'channel_id' => 2, 'app' => 'frontend', 'coolify_service_uuid' => 'frontend-beta'],
|
|
['id' => 52, 'channel_id' => 2, 'app' => 'api', 'coolify_service_uuid' => 'api-beta'],
|
|
],
|
|
'service_sets' => [
|
|
[
|
|
'id' => 53,
|
|
'channel_id' => 2,
|
|
'mode' => 'attach_existing',
|
|
'stack' => [
|
|
'database' => $database,
|
|
'redis' => releaseStatusReadyService('redis', 75),
|
|
'minio' => releaseStatusReadyService('minio', 76),
|
|
],
|
|
],
|
|
],
|
|
'deployments' => [
|
|
['id' => 61, 'channel_id' => 2, 'app' => 'frontend', 'status' => 'deployed'],
|
|
['id' => 62, 'channel_id' => 2, 'app' => 'api', 'status' => 'deployed'],
|
|
],
|
|
]);
|
|
|
|
$channel = releaseStatusChannelBySlug($overview, 'beta');
|
|
|
|
expect($channel['readiness'])->toBe('ready')
|
|
->and($channel['issues'])->toBe([])
|
|
->and(releaseStatusServiceByKey($channel, 'database'))->toMatchArray([
|
|
'status' => 'production_shared',
|
|
'state' => 'ready',
|
|
'severity' => 'ok',
|
|
'issue_type' => null,
|
|
]);
|
|
});
|
|
|
|
it('ignores missing legacy bundles but still blocks on missing versions and URLs', function (): void {
|
|
$overview = releaseStatusOverviewForTest([
|
|
'channels' => [
|
|
[
|
|
'id' => 2,
|
|
'slug' => 'beta',
|
|
'name' => 'Beta',
|
|
'default_channel' => false,
|
|
'enabled' => true,
|
|
'versions' => [],
|
|
'availability' => [
|
|
'configured' => false,
|
|
'status' => 'unconfigured',
|
|
'missing' => [
|
|
'release_bundle',
|
|
'frontend_version',
|
|
'frontend_base_url',
|
|
'api_version',
|
|
'api_base_url',
|
|
],
|
|
],
|
|
],
|
|
],
|
|
]);
|
|
|
|
$channel = releaseStatusChannelBySlug($overview, 'beta');
|
|
$missingLabels = array_column($channel['missing_values'], 'label');
|
|
$missingKeys = array_column($channel['missing_values'], 'key');
|
|
|
|
expect($overview['state'])->toBe('blocked')
|
|
->and($overview['totals']['critical'])->toBeGreaterThanOrEqual(4)
|
|
->and($channel['readiness'])->toBe('blocked')
|
|
->and($missingKeys)->not->toContain('release_bundle')
|
|
->and($missingLabels)->not->toContain('release bundle')
|
|
->and($missingLabels)->toContain('frontend version')
|
|
->and($missingLabels)->toContain('frontend URL')
|
|
->and($missingLabels)->toContain('API version')
|
|
->and($missingLabels)->toContain('API URL')
|
|
->and($overview['issues'][0])->toMatchArray([
|
|
'severity' => 'critical',
|
|
'type' => 'missing_value',
|
|
'channel_slug' => 'beta',
|
|
]);
|
|
});
|
|
|
|
it('marks a branch-based channel ready without a release bundle when app versions and URLs exist', function (): void {
|
|
$versions = releaseStatusReadyVersions(0);
|
|
unset($versions['bundle_id'], $versions['bundle_label']);
|
|
|
|
$overview = releaseStatusOverviewForTest([
|
|
'channels' => [
|
|
[
|
|
'id' => 2,
|
|
'slug' => 'beta',
|
|
'name' => 'Beta',
|
|
'default_channel' => false,
|
|
'enabled' => true,
|
|
'frontend_base_url' => 'https://api-v2.truckwash.io/beta/frontend',
|
|
'api_base_url' => 'https://api-v2.truckwash.io/beta/api',
|
|
'versions' => $versions,
|
|
],
|
|
],
|
|
'deployment_targets' => [
|
|
['id' => 21, 'channel_id' => 2, 'app' => 'frontend', 'coolify_service_uuid' => 'frontend-beta'],
|
|
['id' => 22, 'channel_id' => 2, 'app' => 'api', 'coolify_service_uuid' => 'api-beta'],
|
|
],
|
|
'deployments' => [
|
|
['id' => 61, 'channel_id' => 2, 'app' => 'frontend', 'status' => 'deployed'],
|
|
['id' => 62, 'channel_id' => 2, 'app' => 'api', 'status' => 'deployed'],
|
|
],
|
|
]);
|
|
|
|
$channel = releaseStatusChannelBySlug($overview, 'beta');
|
|
|
|
expect($overview['state'])->toBe('ready')
|
|
->and($channel['readiness'])->toBe('ready')
|
|
->and($channel['availability'])->toMatchArray([
|
|
'configured' => true,
|
|
'missing' => [],
|
|
'status' => 'ready',
|
|
])
|
|
->and($channel['issues'])->toBe([])
|
|
->and($channel['missing_values'])->toBe([]);
|
|
});
|
|
|
|
it('does not report frontend or API URLs missing when target auto endpoints are resolvable', function (): void {
|
|
$overview = releaseStatusOverviewForTest([
|
|
'channels' => [
|
|
[
|
|
'id' => 2,
|
|
'slug' => 'beta',
|
|
'name' => 'Beta',
|
|
'default_channel' => false,
|
|
'enabled' => true,
|
|
'versions' => releaseStatusReadyVersions(),
|
|
],
|
|
],
|
|
'deployment_targets' => [
|
|
[
|
|
'id' => 21,
|
|
'channel_id' => 2,
|
|
'channel_slug' => 'beta',
|
|
'app' => 'frontend',
|
|
'repository' => 'truckwash/front-end-vue',
|
|
'branch' => 'master',
|
|
'coolify_service_uuid' => 'frontend-beta-service',
|
|
'deploy_context' => [
|
|
'endpoint_mode' => 'auto',
|
|
'public_gateway_host' => 'gateway.example.test',
|
|
],
|
|
],
|
|
[
|
|
'id' => 22,
|
|
'channel_id' => 2,
|
|
'channel_slug' => 'beta',
|
|
'app' => 'api',
|
|
'repository' => 'truckwash/backend-php',
|
|
'branch' => 'master',
|
|
'coolify_service_uuid' => 'api-beta-service',
|
|
'deploy_context' => [
|
|
'endpoint_mode' => 'auto',
|
|
'public_gateway_host' => 'gateway.example.test',
|
|
],
|
|
],
|
|
],
|
|
'service_sets' => [
|
|
[
|
|
'id' => 31,
|
|
'channel_id' => 2,
|
|
'mode' => 'isolated_stack',
|
|
'stack' => [
|
|
'database' => releaseStatusReadyService('database', 41),
|
|
'redis' => releaseStatusReadyService('redis', 42),
|
|
'minio' => releaseStatusReadyService('minio', 43),
|
|
],
|
|
],
|
|
],
|
|
]);
|
|
|
|
$channel = releaseStatusChannelBySlug($overview, 'beta');
|
|
$missingKeys = array_column($channel['missing_values'], 'key');
|
|
|
|
expect($missingKeys)->not->toContain('frontend_base_url')
|
|
->and($missingKeys)->not->toContain('api_base_url')
|
|
->and($channel['availability'])->toMatchArray([
|
|
'configured' => true,
|
|
'frontend_base_url' => 'https://gateway.example.test/beta/frontend',
|
|
'api_base_url' => 'https://gateway.example.test/beta/api',
|
|
])
|
|
->and($channel['readiness'])->toBe('ready');
|
|
});
|
|
|
|
it('does not surface legacy release bundle actions as readiness blockers', function (): void {
|
|
$overview = releaseStatusOverviewForTest([
|
|
'channels' => [
|
|
[
|
|
'id' => 20,
|
|
'slug' => 'beta',
|
|
'name' => 'Beta',
|
|
'default_channel' => false,
|
|
'enabled' => true,
|
|
'frontend_base_url' => 'https://beta.example.test',
|
|
'api_base_url' => 'https://api-beta.example.test',
|
|
'versions' => [],
|
|
'availability' => [
|
|
'configured' => false,
|
|
'status' => 'unconfigured',
|
|
'missing' => ['release_bundle'],
|
|
],
|
|
],
|
|
],
|
|
'bundles' => [
|
|
['id' => 91, 'channel_id' => 20, 'status' => 'deployed', 'version_label' => 'beta-bundle'],
|
|
['id' => 92, 'channel_id' => 20, 'status' => 'failed', 'version_label' => 'failed-bundle'],
|
|
],
|
|
]);
|
|
|
|
$channel = releaseStatusChannelBySlug($overview, 'beta');
|
|
|
|
expect($channel['readiness'])->toBe('ready')
|
|
->and($channel['availability'])->toMatchArray([
|
|
'configured' => true,
|
|
'missing' => [],
|
|
'status' => 'ready',
|
|
])
|
|
->and($channel['issues'])->toBe([])
|
|
->and($channel['missing_values'])->toBe([]);
|
|
});
|
|
|
|
it('surfaces failed latest deployments as critical promotion blockers', function (): void {
|
|
$overview = releaseStatusOverviewForTest([
|
|
'channels' => [
|
|
[
|
|
'id' => 3,
|
|
'slug' => 'canary',
|
|
'name' => 'Canary',
|
|
'default_channel' => false,
|
|
'enabled' => true,
|
|
'frontend_base_url' => 'https://canary.example.test',
|
|
'api_base_url' => 'https://api-canary.example.test',
|
|
'versions' => releaseStatusReadyVersions(9),
|
|
'availability' => [
|
|
'configured' => true,
|
|
'status' => 'ready',
|
|
'missing' => [],
|
|
],
|
|
],
|
|
],
|
|
'deployment_targets' => [
|
|
['id' => 51, 'channel_id' => 3, 'app' => 'frontend', 'repository' => 'truckwash/front-end-vue', 'coolify_service_uuid' => 'frontend-canary'],
|
|
['id' => 52, 'channel_id' => 3, 'app' => 'api', 'repository' => 'truckwash/backend-php', 'coolify_service_uuid' => 'api-canary'],
|
|
],
|
|
'service_sets' => [
|
|
[
|
|
'id' => 53,
|
|
'channel_id' => 3,
|
|
'mode' => 'isolated_stack',
|
|
'stack' => [
|
|
'database' => releaseStatusReadyService('database', 54),
|
|
'redis' => releaseStatusReadyService('redis', 55),
|
|
'minio' => releaseStatusReadyService('minio', 56),
|
|
],
|
|
],
|
|
],
|
|
'deployments' => [
|
|
[
|
|
'id' => 57,
|
|
'channel_id' => 3,
|
|
'app' => 'api',
|
|
'status' => 'failed',
|
|
'failure_summary' => [
|
|
'root_cause' => 'Composer install failed.',
|
|
'next_action' => 'Open the deployment logs and fix composer dependencies.',
|
|
],
|
|
],
|
|
['id' => 58, 'channel_id' => 3, 'app' => 'frontend', 'status' => 'deployed'],
|
|
],
|
|
]);
|
|
|
|
$channel = releaseStatusChannelBySlug($overview, 'canary');
|
|
$api = releaseStatusServiceByKey($channel, 'api');
|
|
|
|
expect($overview['state'])->toBe('blocked')
|
|
->and($channel['readiness'])->toBe('blocked')
|
|
->and($api)->toMatchArray([
|
|
'severity' => 'critical',
|
|
'state' => 'failed',
|
|
'issue_type' => 'failed_deployment',
|
|
'deployment_id' => 57,
|
|
])
|
|
->and($channel['issues'][0])->toMatchArray([
|
|
'severity' => 'critical',
|
|
'type' => 'failed_deployment',
|
|
'deployment_id' => 57,
|
|
'next_action' => 'Open the deployment logs and fix composer dependencies.',
|
|
])
|
|
->and($channel['issues'][0]['key'])->toBe('failed_deployment:3:api::57:')
|
|
->and($channel['issues'][0]['impact'])->toContain('cannot be promoted')
|
|
->and(array_column($channel['issues'][0]['actions'], 'id'))->toContain('retry_deployment');
|
|
});
|
|
|
|
it('offers application target preparation for path routed Coolify failures', function (): void {
|
|
$overview = releaseStatusOverviewForTest([
|
|
'channels' => [
|
|
[
|
|
'id' => 30,
|
|
'slug' => 'internal',
|
|
'name' => 'Internal',
|
|
'default_channel' => false,
|
|
'enabled' => true,
|
|
'frontend_base_url' => 'https://internal.example.test',
|
|
'api_base_url' => 'https://api-internal.example.test',
|
|
'versions' => releaseStatusReadyVersions(19),
|
|
'availability' => [
|
|
'configured' => true,
|
|
'status' => 'ready',
|
|
'missing' => [],
|
|
],
|
|
],
|
|
],
|
|
'deployment_targets' => [
|
|
['id' => 81, 'channel_id' => 30, 'app' => 'frontend', 'repository' => 'truckwash/front-end-vue', 'coolify_service_uuid' => 'frontend-internal'],
|
|
['id' => 82, 'channel_id' => 30, 'app' => 'api', 'repository' => 'truckwash/backend-php', 'coolify_service_uuid' => 'legacy-api-service'],
|
|
],
|
|
'service_sets' => [
|
|
[
|
|
'id' => 83,
|
|
'channel_id' => 30,
|
|
'mode' => 'isolated_stack',
|
|
'stack' => [
|
|
'database' => releaseStatusReadyService('database', 84),
|
|
'redis' => releaseStatusReadyService('redis', 85),
|
|
'minio' => releaseStatusReadyService('minio', 86),
|
|
],
|
|
],
|
|
],
|
|
'deployments' => [
|
|
[
|
|
'id' => 87,
|
|
'channel_id' => 30,
|
|
'app' => 'api',
|
|
'status' => 'failed',
|
|
'failure_summary' => [
|
|
'root_cause' => 'Path-routed release targets require a Coolify application resource with StripPrefix labels.',
|
|
'next_action' => 'Set coolify_resource_type=application or migrate this target before deploying.',
|
|
],
|
|
],
|
|
['id' => 88, 'channel_id' => 30, 'app' => 'frontend', 'status' => 'deployed'],
|
|
],
|
|
]);
|
|
|
|
$channel = releaseStatusChannelBySlug($overview, 'internal');
|
|
$actions = $channel['issues'][0]['actions'];
|
|
$actionIds = array_column($actions, 'id');
|
|
$prepareAction = $actions[array_search('prepare_application_target', $actionIds, true)];
|
|
|
|
expect($actionIds)->toContain('retry_deployment')
|
|
->and($actionIds)->toContain('prepare_application_target')
|
|
->and($prepareAction)->toMatchArray([
|
|
'requires_confirmation' => true,
|
|
'requires_input' => false,
|
|
'disabled_reason' => '',
|
|
]);
|
|
});
|
|
|
|
it('flags isolated stacks that are missing database Redis and MinIO services', function (): void {
|
|
$overview = releaseStatusOverviewForTest([
|
|
'channels' => [
|
|
[
|
|
'id' => 4,
|
|
'slug' => 'internal',
|
|
'name' => 'Internal',
|
|
'default_channel' => false,
|
|
'enabled' => true,
|
|
'frontend_base_url' => 'https://internal.example.test',
|
|
'api_base_url' => 'https://api-internal.example.test',
|
|
'versions' => releaseStatusReadyVersions(11),
|
|
'availability' => [
|
|
'configured' => true,
|
|
'status' => 'ready',
|
|
'missing' => [],
|
|
],
|
|
],
|
|
],
|
|
'deployment_targets' => [
|
|
['id' => 61, 'channel_id' => 4, 'app' => 'frontend', 'coolify_service_uuid' => 'frontend-internal'],
|
|
['id' => 62, 'channel_id' => 4, 'app' => 'api', 'coolify_service_uuid' => 'api-internal'],
|
|
],
|
|
'service_sets' => [
|
|
[
|
|
'id' => 63,
|
|
'channel_id' => 4,
|
|
'mode' => 'isolated_stack',
|
|
'stack' => [
|
|
'frontend' => ['id' => 61],
|
|
'api' => ['id' => 62],
|
|
],
|
|
'data_services' => [],
|
|
],
|
|
],
|
|
]);
|
|
|
|
$channel = releaseStatusChannelBySlug($overview, 'internal');
|
|
|
|
expect($channel['readiness'])->toBe('blocked')
|
|
->and(releaseStatusServiceByKey($channel, 'database'))->toMatchArray([
|
|
'severity' => 'critical',
|
|
'issue_type' => 'missing_value',
|
|
'missing_key' => 'database_service',
|
|
])
|
|
->and(releaseStatusServiceByKey($channel, 'redis'))->toMatchArray([
|
|
'severity' => 'critical',
|
|
'issue_type' => 'missing_value',
|
|
'missing_key' => 'redis_service',
|
|
])
|
|
->and(releaseStatusServiceByKey($channel, 'minio'))->toMatchArray([
|
|
'severity' => 'critical',
|
|
'issue_type' => 'missing_value',
|
|
'missing_key' => 'minio_service',
|
|
])
|
|
->and(array_column($channel['issues'][0]['actions'], 'id'))->toContain('complete_data_services');
|
|
});
|
|
|
|
it('maps degraded Coolify targets and reconcile failures to release service issues', function (): void {
|
|
$database = releaseStatusReadyService('database', 71);
|
|
$database['deployment_status'] = 'reconcile_failed';
|
|
$database['availability_state'] = 'degraded';
|
|
|
|
$overview = releaseStatusOverviewForTest([
|
|
'channels' => [
|
|
[
|
|
'id' => 5,
|
|
'slug' => 'preview',
|
|
'name' => 'Preview',
|
|
'default_channel' => false,
|
|
'enabled' => true,
|
|
'frontend_base_url' => 'https://preview.example.test',
|
|
'api_base_url' => 'https://api-preview.example.test',
|
|
'versions' => releaseStatusReadyVersions(12),
|
|
'availability' => [
|
|
'configured' => true,
|
|
'status' => 'ready',
|
|
'missing' => [],
|
|
],
|
|
],
|
|
],
|
|
'deployment_targets' => [
|
|
['id' => 72, 'channel_id' => 5, 'app' => 'frontend', 'coolify_service_uuid' => 'frontend-preview'],
|
|
['id' => 73, 'channel_id' => 5, 'app' => 'api', 'coolify_service_uuid' => 'api-preview'],
|
|
],
|
|
'service_sets' => [
|
|
[
|
|
'id' => 74,
|
|
'channel_id' => 5,
|
|
'mode' => 'isolated_stack',
|
|
'stack' => [
|
|
'database' => $database,
|
|
'redis' => releaseStatusReadyService('redis', 75),
|
|
'minio' => releaseStatusReadyService('minio', 76),
|
|
],
|
|
],
|
|
],
|
|
]);
|
|
|
|
$channel = releaseStatusChannelBySlug($overview, 'preview');
|
|
$databaseRow = releaseStatusServiceByKey($channel, 'database');
|
|
|
|
expect($databaseRow)->toMatchArray([
|
|
'severity' => 'critical',
|
|
'state' => 'service_unhealthy',
|
|
'issue_type' => 'service_unhealthy',
|
|
'coolify_target_id' => 71,
|
|
])
|
|
->and($channel['issues'][0])->toMatchArray([
|
|
'severity' => 'critical',
|
|
'type' => 'service_unhealthy',
|
|
'service_key' => 'database',
|
|
])
|
|
->and(array_column($channel['issues'][0]['actions'], 'id'))->toContain('reconcile_coolify_target')
|
|
->and(array_column($channel['issues'][0]['actions'], 'id'))->toContain('redeploy_coolify_target')
|
|
->and(array_column($channel['issues'][0]['actions'], 'id'))->toContain('restart_coolify_target');
|
|
});
|