diff --git a/services/nginx/app/classes/coolify_manager.php b/services/nginx/app/classes/coolify_manager.php index 97609f5a..a3e22b10 100644 --- a/services/nginx/app/classes/coolify_manager.php +++ b/services/nginx/app/classes/coolify_manager.php @@ -53,6 +53,7 @@ class coolify_manager 'targets' => $this->listTargets(), 'availability' => $this->availabilitySummary(), 'load_balancer' => $this->loadBalancerSummary(), + 'github_runners' => $this->githubRunnerSummary(), ]; } @@ -1371,6 +1372,54 @@ class coolify_manager ]; } + public function githubRunnerSummary(): array + { + $this->ensureSchema(); + $repositories = $this->githubRunnerRepositories([]); + $labels = $this->githubRunnerLabels(null); + $countPerRepo = $this->githubRunnerCount(null); + $serviceUuid = $this->nullableString($this->coolifyConfigValue('github_runner_service_uuid', '')); + $token = $this->githubRunnerToken([]); + $config = [ + 'configured' => $serviceUuid !== null, + 'service_uuid' => $serviceUuid, + 'service_name' => $this->githubRunnerServiceName(null), + 'repositories' => $repositories, + 'labels' => $labels, + 'count_per_repo' => $countPerRepo, + 'desired_runner_count' => count($repositories) * $countPerRepo, + 'compose_hash' => $this->composeHash($this->githubRunnerComposeTemplate($repositories, $labels, $countPerRepo)), + 'token_set' => $token !== '', + 'token_source' => trim((string)(getenv('GITHUB_RUNNER_TOKEN') ?: getenv('GITHUB_TOKEN') ?: '')) !== '' ? 'env' : ($token !== '' ? 'config' : null), + ]; + + $base = [ + 'status' => $serviceUuid === null ? 'not_configured' : 'configured', + 'config' => $config, + 'service' => null, + 'last_error' => null, + ]; + + if ($serviceUuid === null || !coolify_schema_bootstrap::tablesExist()) { + return $base; + } + + try { + $instanceId = $this->defaultInstanceId(); + $instance = $this->getInstance($instanceId); + $service = $this->clientForInstance($instance)->getService($serviceUuid); + return array_replace($base, [ + 'status' => 'ok', + 'service' => self::redactCoolifyResponse($service), + ]); + } catch (Throwable $throwable) { + return array_replace($base, [ + 'status' => 'unknown', + 'last_error' => $throwable->getMessage(), + ]); + } + } + public function deployGithubRunners(array $input, ?int $actorUserId = null): array { $this->ensureSchema(); diff --git a/services/nginx/app/modules/coolify/coolify_c.php b/services/nginx/app/modules/coolify/coolify_c.php index 68df3b39..5d10c646 100644 --- a/services/nginx/app/modules/coolify/coolify_c.php +++ b/services/nginx/app/modules/coolify/coolify_c.php @@ -8,10 +8,22 @@ require_once WD . '/modules/coolify/config/coolify_hetzner_load_balancer_id_c.ph require_once WD . '/modules/coolify/config/coolify_lb_automation_enabled_c.php'; require_once WD . '/modules/coolify/config/coolify_lb_automation_mode_c.php'; require_once WD . '/modules/coolify/config/coolify_public_gateway_host_c.php'; +require_once WD . '/modules/coolify/config/coolify_github_runner_backend_repository_c.php'; +require_once WD . '/modules/coolify/config/coolify_github_runner_count_per_repo_c.php'; +require_once WD . '/modules/coolify/config/coolify_github_runner_frontend_repository_c.php'; +require_once WD . '/modules/coolify/config/coolify_github_runner_labels_c.php'; +require_once WD . '/modules/coolify/config/coolify_github_runner_service_uuid_c.php'; +require_once WD . '/modules/coolify/config/coolify_github_runner_token_c.php'; use modules\coolify\config\coolify_hetzner_cloud_api_token_c; use modules\coolify\config\coolify_hetzner_load_balancer_id_c; use modules\coolify\config\coolify_enabled_c; +use modules\coolify\config\coolify_github_runner_backend_repository_c; +use modules\coolify\config\coolify_github_runner_count_per_repo_c; +use modules\coolify\config\coolify_github_runner_frontend_repository_c; +use modules\coolify\config\coolify_github_runner_labels_c; +use modules\coolify\config\coolify_github_runner_service_uuid_c; +use modules\coolify\config\coolify_github_runner_token_c; use modules\coolify\config\coolify_lb_automation_enabled_c; use modules\coolify\config\coolify_lb_automation_mode_c; use modules\coolify\config\coolify_public_gateway_host_c; @@ -29,6 +41,12 @@ class coolify_c public coolify_hetzner_load_balancer_id_c $hetzner_load_balancer_id; public coolify_hetzner_cloud_api_token_c $hetzner_cloud_api_token; public coolify_public_gateway_host_c $public_gateway_host; + public coolify_github_runner_token_c $github_runner_token; + public coolify_github_runner_service_uuid_c $github_runner_service_uuid; + public coolify_github_runner_frontend_repository_c $github_runner_frontend_repository; + public coolify_github_runner_backend_repository_c $github_runner_backend_repository; + public coolify_github_runner_labels_c $github_runner_labels; + public coolify_github_runner_count_per_repo_c $github_runner_count_per_repo; public function __construct() { @@ -40,6 +58,12 @@ class coolify_c coolify_hetzner_load_balancer_id_c::class, coolify_hetzner_cloud_api_token_c::class, coolify_public_gateway_host_c::class, + coolify_github_runner_token_c::class, + coolify_github_runner_service_uuid_c::class, + coolify_github_runner_frontend_repository_c::class, + coolify_github_runner_backend_repository_c::class, + coolify_github_runner_labels_c::class, + coolify_github_runner_count_per_repo_c::class, ]); $this->enabled = new coolify_enabled_c(); $this->lb_automation_enabled = new coolify_lb_automation_enabled_c(); @@ -47,12 +71,18 @@ class coolify_c $this->hetzner_load_balancer_id = new coolify_hetzner_load_balancer_id_c(); $this->hetzner_cloud_api_token = new coolify_hetzner_cloud_api_token_c(); $this->public_gateway_host = new coolify_public_gateway_host_c(); + $this->github_runner_token = new coolify_github_runner_token_c(); + $this->github_runner_service_uuid = new coolify_github_runner_service_uuid_c(); + $this->github_runner_frontend_repository = new coolify_github_runner_frontend_repository_c(); + $this->github_runner_backend_repository = new coolify_github_runner_backend_repository_c(); + $this->github_runner_labels = new coolify_github_runner_labels_c(); + $this->github_runner_count_per_repo = new coolify_github_runner_count_per_repo_c(); } public function getConfigRequest(): array { return array_map(static function (array $row): array { - if (($row['variable'] ?? '') === 'hetzner_cloud_api_token') { + if (in_array(($row['variable'] ?? ''), ['hetzner_cloud_api_token', 'github_runner_token'], true)) { $secretSet = trim((string)($row['value'] ?? '')) !== ''; $row['value'] = $secretSet ? '[redacted]' : ''; $row['secret_set'] = $secretSet; diff --git a/services/nginx/app/openapi.yaml b/services/nginx/app/openapi.yaml index 1a675cbe..17babcb5 100644 --- a/services/nginx/app/openapi.yaml +++ b/services/nginx/app/openapi.yaml @@ -11217,6 +11217,46 @@ paths: '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } + + /superuser/coolify/github-runners: + get: + tags: + - Superuser + summary: Coolify-managed GitHub Actions runner state + operationId: getSuperuserCoolifyGithubRunners + responses: + '200': + description: GitHub runner summary returned successfully + content: + application/json: + schema: + $ref: '#/components/schemas/SuperuserCoolifyGithubRunnerResponse' + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + + /superuser/coolify/github-runners/deploy: + post: + tags: + - Superuser + summary: Deploy Coolify-managed GitHub Actions runners + operationId: deploySuperuserCoolifyGithubRunners + requestBody: + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/SuperuserCoolifyGithubRunnerDeployRequest' + responses: + '200': + description: GitHub runner deployment result returned + content: + application/json: + schema: + $ref: '#/components/schemas/SuperuserCoolifyGithubRunnerDeployResponse' + '409': { $ref: '#/components/responses/Conflict' } + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + /superuser/coolify/gateways: get: tags: @@ -13512,6 +13552,8 @@ components: additionalProperties: true load_balancer: $ref: '#/components/schemas/SuperuserCoolifyLoadBalancer' + github_runners: + $ref: '#/components/schemas/SuperuserCoolifyGithubRunnerSummary' meta: type: object additionalProperties: true @@ -13564,6 +13606,126 @@ components: type: object additionalProperties: true + + SuperuserCoolifyGithubRunnerResponse: + type: object + properties: + success: + type: boolean + data: + $ref: '#/components/schemas/SuperuserCoolifyGithubRunnerSummary' + meta: + type: object + additionalProperties: true + includes: + type: object + additionalProperties: true + + SuperuserCoolifyGithubRunnerDeployRequest: + type: object + properties: + dry_run: + type: boolean + default: true + enforce: + type: boolean + default: false + instance_id: + type: integer + service_uuid: + type: string + service_name: + type: string + frontend_repository: + type: string + example: copenhagentruckwash/pleno-vue + backend_repository: + type: string + example: copenhagentruckwash/api + labels: + type: string + example: self-hosted,Linux,X64,default + count_per_repo: + type: integer + minimum: 1 + maximum: 10 + default: 1 + github_token: + type: string + writeOnly: true + persist_token: + type: boolean + default: false + project_uuid: + type: string + environment_name: + type: string + environment_uuid: + type: string + server_uuid: + type: string + destination_uuid: + type: string + + SuperuserCoolifyGithubRunnerDeployResponse: + type: object + properties: + success: + type: boolean + data: + type: object + additionalProperties: true + meta: + type: object + additionalProperties: true + includes: + type: object + additionalProperties: true + + SuperuserCoolifyGithubRunnerSummary: + type: object + properties: + status: + type: string + enum: [not_configured, configured, ok, unknown] + config: + type: object + properties: + configured: + type: boolean + service_uuid: + type: string + nullable: true + service_name: + type: string + repositories: + type: object + additionalProperties: + type: string + labels: + type: array + items: + type: string + count_per_repo: + type: integer + desired_runner_count: + type: integer + compose_hash: + type: string + token_set: + type: boolean + token_source: + type: string + nullable: true + enum: [env, config] + service: + type: object + nullable: true + additionalProperties: true + last_error: + type: string + nullable: true + SuperuserCoolifyLoadBalancerResponse: type: object properties: diff --git a/services/nginx/app/routes/superuserCoolifyRoute.php b/services/nginx/app/routes/superuserCoolifyRoute.php index 7cab8d70..07def30c 100644 --- a/services/nginx/app/routes/superuserCoolifyRoute.php +++ b/services/nginx/app/routes/superuserCoolifyRoute.php @@ -97,6 +97,40 @@ class superuserCoolifyRoute 'superuser_coolify_manage' => 'Deploy the latest Coolify API code for the public gateway host', ]); + + $this->get('/superuser/coolify/github-runners', function () { + global $response; + + $this->requirePermission('superuser_coolify_view'); + $response->success((new coolify_manager())->githubRunnerSummary()); + }, [ + 'superuser_coolify_view' => 'View Coolify-managed GitHub Actions runner state', + ]); + + $this->post('/superuser/coolify/github-runners/deploy', function () { + global $response; + + $this->requirePermission('superuser_coolify_manage'); + try { + $parameters = $this->getParametersAsArray(); + $parameters['dry_run'] = array_key_exists('dry_run', $parameters) + ? filter_var($parameters['dry_run'], FILTER_VALIDATE_BOOLEAN) + : !filter_var($parameters['enforce'] ?? false, FILTER_VALIDATE_BOOLEAN); + $result = (new coolify_manager())->deployGithubRunners( + $parameters, + $this->actorUserId() + ); + if (($result['ok'] ?? false) !== true) { + $response->error($result, 409); + } + $response->success($result); + } catch (Throwable $throwable) { + $response->error(['message' => $throwable->getMessage()], 409); + } + }, [ + 'superuser_coolify_manage' => 'Create or update the Coolify-managed GitHub Actions runner service', + ]); + $this->get('/superuser/coolify/gateways', function () { global $response; diff --git a/services/nginx/app/tests/Unit/Coolify/CoolifyManagerTest.php b/services/nginx/app/tests/Unit/Coolify/CoolifyManagerTest.php index 6dbeb714..310b4183 100644 --- a/services/nginx/app/tests/Unit/Coolify/CoolifyManagerTest.php +++ b/services/nginx/app/tests/Unit/Coolify/CoolifyManagerTest.php @@ -587,6 +587,12 @@ it('defines Coolify schema, route permissions, and replication integration hooks expect($schema)->toContain('65.21.214.30'); expect($schema)->toContain('23.88.23.183'); expect($schema)->toContain("ensureModuleConfigDefault('Coolify', 'enabled'"); + expect($schema)->toContain("ensureModuleConfigDefault('Coolify', 'github_runner_token'"); + expect($schema)->toContain("ensureModuleConfigDefault('Coolify', 'github_runner_service_uuid'"); + expect($schema)->toContain("ensureModuleConfigDefault('Coolify', 'github_runner_frontend_repository'"); + expect($schema)->toContain("ensureModuleConfigDefault('Coolify', 'github_runner_backend_repository'"); + expect($schema)->toContain("ensureModuleConfigDefault('Coolify', 'github_runner_labels'"); + expect($schema)->toContain("ensureModuleConfigDefault('Coolify', 'github_runner_count_per_repo'"); expect($route)->toContain('/superuser/coolify'); expect($route)->toContain('/superuser/coolify/load-balancer'); @@ -594,6 +600,8 @@ it('defines Coolify schema, route permissions, and replication integration hooks expect($route)->toContain('/superuser/coolify/load-balancer/routes/deploy'); expect($route)->toContain('/superuser/coolify/load-balancer/api/deploy'); expect($route)->toContain('/superuser/coolify/gateways'); + expect($route)->toContain('/superuser/coolify/github-runners'); + expect($route)->toContain('/superuser/coolify/github-runners/deploy'); expect($route)->toContain('/superuser/coolify/gateways/{id}/test'); expect($route)->toContain('/superuser/coolify/instances/{id}/test'); expect($route)->toContain('/superuser/coolify/instances/{id}/placement'); @@ -660,6 +668,8 @@ it('defines Coolify schema, route permissions, and replication integration hooks expect($manager)->toContain('reconcileLoadBalancer'); expect($manager)->toContain('deployGatewayApplicationRoutes'); expect($manager)->toContain('deployGatewayApiCode'); + expect($manager)->toContain('deployGithubRunners'); + expect($manager)->toContain('githubRunnerSummary'); expect($manager)->toContain('gateway_api_code_deploy'); expect($manager)->toContain('deploy_gateway_route_after_code'); expect($manager)->toContain('loadBalancerReleaseGatewayTargets'); @@ -723,6 +733,8 @@ it('defines Coolify schema, route permissions, and replication integration hooks expect($cron)->toContain('CoolifyLoadBalancerReconcileCron'); expect($coolifyConfig)->toContain('[redacted]'); expect($coolifyConfig)->toContain('secret_set'); + expect($coolifyConfig)->toContain('github_runner_token'); + expect($coolifyConfig)->toContain('github_runner_service_uuid'); expect($tokenConfig)->toContain('replication_secret_box::encrypt'); expect($openapi)->toContain('/superuser/coolify:'); expect($openapi)->toContain('operationId: getSuperuserCoolifyLoadBalancer'); @@ -730,10 +742,13 @@ it('defines Coolify schema, route permissions, and replication integration hooks expect($openapi)->toContain('operationId: deploySuperuserCoolifyGatewayRoutes'); expect($openapi)->toContain('operationId: deploySuperuserCoolifyGatewayApiCode'); expect($openapi)->toContain('operationId: listSuperuserCoolifyGateways'); + expect($openapi)->toContain('operationId: getSuperuserCoolifyGithubRunners'); + expect($openapi)->toContain('operationId: deploySuperuserCoolifyGithubRunners'); expect($openapi)->toContain('operationId: testSuperuserCoolifyGateway'); expect($openapi)->toContain('operationId: discoverSuperuserCoolifyInstancePlacement'); expect($openapi)->toContain('operationId: createSuperuserCoolifyTarget'); expect($openapi)->toContain('SuperuserCoolifyTarget'); expect($openapi)->toContain('SuperuserCoolifyLoadBalancer'); + expect($openapi)->toContain('SuperuserCoolifyGithubRunnerSummary'); expect($openapi)->toContain('SuperuserCoolifyGateway'); });