Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4dd49208a | ||
|
|
6d739cfebc | ||
|
|
20071166f8 | ||
|
|
c23168afc5 | ||
|
|
72704b7806 | ||
|
|
f7485f0767 |
@@ -9,8 +9,8 @@ on:
|
||||
|
||||
jobs:
|
||||
qodana:
|
||||
# Use GitHub-hosted runners for PR scans so untrusted code never runs on persistent internal infrastructure.
|
||||
runs-on: ubuntu-latest
|
||||
# CI runs on the repository's self-hosted runner pool.
|
||||
runs-on: [self-hosted, Linux, X64, default]
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
@@ -9,7 +9,7 @@ on:
|
||||
|
||||
jobs:
|
||||
assign-task:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: [self-hosted, Linux, X64, default]
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
jobs:
|
||||
php:
|
||||
name: PHP ${{ matrix.suite }} (required)
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: [self-hosted, Linux, X64, default]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
|
||||
edge-agent:
|
||||
name: Edge Agent (required)
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: [self-hosted, Linux, X64, default]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -91,7 +91,7 @@ jobs:
|
||||
|
||||
edge-broker:
|
||||
name: Edge Broker (required)
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: [self-hosted, Linux, X64, default]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -125,7 +125,7 @@ jobs:
|
||||
|
||||
edge-gateway-backend:
|
||||
name: Edge Gateway Backend (required)
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: [self-hosted, Linux, X64, default]
|
||||
env:
|
||||
COMPOSE_FILE: docker-compose.yml:.github/docker-compose.ci.yml
|
||||
COMPOSE_PROJECT_NAME: edge-gateway-backend-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
@@ -258,7 +258,7 @@ jobs:
|
||||
|
||||
release-manager-gate:
|
||||
name: Release Manager gate
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: [self-hosted, Linux, X64, default]
|
||||
needs: [php, edge-agent, edge-broker, edge-gateway-backend]
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
|
||||
|
||||
|
||||
+41
-1
@@ -8869,6 +8869,47 @@ paths:
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
|
||||
/modules/self-serve/lane/wash/my-active-wash:
|
||||
get:
|
||||
tags:
|
||||
- Modules
|
||||
summary: Get the authenticated customer's active self-serve wash
|
||||
description: |
|
||||
Returns the latest active self-serve wash for the authenticated customer,
|
||||
without requiring the frontend to know or poll a lane id.
|
||||
operationId: getMyActiveSelfServeWash
|
||||
responses:
|
||||
'200':
|
||||
description: Active self-serve wash details resolved
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
lane_id:
|
||||
type: integer
|
||||
status:
|
||||
type: string
|
||||
in_progress:
|
||||
type: boolean
|
||||
elapsed_minutes:
|
||||
type: integer
|
||||
session:
|
||||
type: object
|
||||
nullable: true
|
||||
customer:
|
||||
type: object
|
||||
nullable: true
|
||||
vehicle:
|
||||
type: object
|
||||
nullable: true
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
|
||||
/modules/self-serve/sessions:
|
||||
get:
|
||||
tags:
|
||||
@@ -18714,4 +18755,3 @@ components:
|
||||
data:
|
||||
$ref: '#/components/schemas/DepartmentDailyReportOutsideHoursTrendPayload'
|
||||
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ class coolify_manager
|
||||
'targets' => $this->listTargets(),
|
||||
'availability' => $this->availabilitySummary(),
|
||||
'load_balancer' => $this->loadBalancerSummary(),
|
||||
'github_runners' => $this->githubRunnerSummary(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1371,6 +1372,177 @@ 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();
|
||||
|
||||
$dryRun = $this->toBool($input['dry_run'] ?? null, false);
|
||||
$instanceId = (int)($input['instance_id'] ?? 0);
|
||||
if ($instanceId <= 0) {
|
||||
$instanceId = $this->defaultInstanceId();
|
||||
}
|
||||
$instance = $this->getInstance($instanceId);
|
||||
$repositories = $this->githubRunnerRepositories($input);
|
||||
$labels = $this->githubRunnerLabels($input['labels'] ?? null);
|
||||
$countPerRepo = $this->githubRunnerCount($input['count_per_repo'] ?? $input['runner_count_per_repo'] ?? null);
|
||||
$serviceName = $this->githubRunnerServiceName($input['service_name'] ?? null);
|
||||
$resourceUuid = $this->nullableString($input['service_uuid'] ?? null)
|
||||
?? $this->nullableString($this->coolifyConfigValue('github_runner_service_uuid', ''));
|
||||
$token = $this->githubRunnerToken($input);
|
||||
$template = $this->githubRunnerComposeTemplate($repositories, $labels, $countPerRepo);
|
||||
$hash = $this->composeHash($template);
|
||||
|
||||
$plan = [
|
||||
'type' => 'deploy_github_runners',
|
||||
'instance_id' => $instanceId,
|
||||
'service_uuid' => $resourceUuid,
|
||||
'service_name' => $serviceName,
|
||||
'repositories' => $repositories,
|
||||
'labels' => $labels,
|
||||
'count_per_repo' => $countPerRepo,
|
||||
'compose_hash' => $hash,
|
||||
'action' => $resourceUuid === null ? 'create' : 'update',
|
||||
'token_set' => $token !== '',
|
||||
'token_source' => trim((string)($input['github_token'] ?? $input['token'] ?? '')) !== '' ? 'request' : 'config',
|
||||
];
|
||||
|
||||
if ($dryRun) {
|
||||
$this->audit(null, $instanceId, null, 'github_runners_planned', $actorUserId, 'info', $plan);
|
||||
return [
|
||||
'ok' => true,
|
||||
'dry_run' => true,
|
||||
'mutated' => false,
|
||||
'planned' => [$plan],
|
||||
'applied' => [],
|
||||
'errors' => [],
|
||||
'service_uuid' => $resourceUuid,
|
||||
'service_name' => $serviceName,
|
||||
'compose_hash' => $hash,
|
||||
'repositories' => $repositories,
|
||||
'labels' => $labels,
|
||||
'count_per_repo' => $countPerRepo,
|
||||
];
|
||||
}
|
||||
|
||||
if ($token === '') {
|
||||
throw new RuntimeException('GitHub runner token is required to deploy self-hosted runners.');
|
||||
}
|
||||
|
||||
$client = $this->clientForInstance($instance);
|
||||
$apiResult = [];
|
||||
$action = $resourceUuid === null ? 'created' : 'updated';
|
||||
if ($resourceUuid === null) {
|
||||
$apiResult = $client->createService($this->githubRunnerServicePayload($instance, $input, $serviceName, $template, false));
|
||||
$resourceUuid = trim((string)($apiResult['uuid'] ?? ''));
|
||||
if ($resourceUuid === '') {
|
||||
throw new RuntimeException('Coolify did not return a GitHub runner service UUID.');
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
$apiResult = $client->updateService($resourceUuid, $this->githubRunnerServicePayload($instance, $input, $serviceName, $template, true));
|
||||
} catch (Throwable $throwable) {
|
||||
if (!str_contains(strtolower($throwable->getMessage()), '404')
|
||||
&& !str_contains(strtolower($throwable->getMessage()), 'not found')) {
|
||||
throw $throwable;
|
||||
}
|
||||
$apiResult = $client->createService($this->githubRunnerServicePayload($instance, $input, $serviceName, $template, false));
|
||||
$resourceUuid = trim((string)($apiResult['uuid'] ?? ''));
|
||||
if ($resourceUuid === '') {
|
||||
throw new RuntimeException('Coolify did not return a GitHub runner service UUID.');
|
||||
}
|
||||
$action = 'created';
|
||||
}
|
||||
}
|
||||
|
||||
$client->updateServiceEnvsBulk($resourceUuid, ['GITHUB_RUNNER_TOKEN' => $token]);
|
||||
$start = $this->startOrRestartService($client, $resourceUuid, $action === 'updated');
|
||||
$deployment = $client->deployResource($resourceUuid, false);
|
||||
|
||||
$this->setModuleConfigValue('Coolify', 'github_runner_service_uuid', $resourceUuid, 'string');
|
||||
$this->setModuleConfigValue('Coolify', 'github_runner_frontend_repository', $repositories['frontend'], 'string');
|
||||
$this->setModuleConfigValue('Coolify', 'github_runner_backend_repository', $repositories['backend'], 'string');
|
||||
$this->setModuleConfigValue('Coolify', 'github_runner_labels', implode(',', $labels), 'string');
|
||||
$this->setModuleConfigValue('Coolify', 'github_runner_count_per_repo', (string)$countPerRepo, 'int');
|
||||
if ($this->toBool($input['persist_token'] ?? null, false)) {
|
||||
$this->setModuleConfigValue('Coolify', 'github_runner_token', replication_secret_box::encrypt($token), 'string');
|
||||
}
|
||||
|
||||
$applied = array_replace($plan, [
|
||||
'action' => $action,
|
||||
'service_uuid' => $resourceUuid,
|
||||
'coolify' => self::redactCoolifyResponse($apiResult),
|
||||
'start' => self::redactCoolifyResponse(is_array($start) ? $start : []),
|
||||
'deployment' => self::redactCoolifyResponse($deployment),
|
||||
]);
|
||||
|
||||
$this->audit(null, $instanceId, null, 'github_runners_deployed', $actorUserId, 'info', $applied);
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'dry_run' => false,
|
||||
'mutated' => true,
|
||||
'planned' => [$plan],
|
||||
'applied' => [$applied],
|
||||
'errors' => [],
|
||||
'action' => $action,
|
||||
'service_uuid' => $resourceUuid,
|
||||
'service_name' => $serviceName,
|
||||
'compose_hash' => $hash,
|
||||
'repositories' => $repositories,
|
||||
'labels' => $labels,
|
||||
'count_per_repo' => $countPerRepo,
|
||||
'deployment' => self::redactCoolifyResponse($deployment),
|
||||
];
|
||||
}
|
||||
|
||||
private function gatewayApiCodeVersionLabel(array $target): string
|
||||
{
|
||||
$channelSlug = trim((string)($target['channel_slug'] ?? 'gateway'));
|
||||
@@ -3266,6 +3438,141 @@ class coolify_manager
|
||||
];
|
||||
}
|
||||
|
||||
private function githubRunnerRepositories(array $input): array
|
||||
{
|
||||
return [
|
||||
'frontend' => $this->normalizeGithubRepository(
|
||||
$input['frontend_repository'] ?? $input['frontend_repo'] ?? $this->coolifyConfigValue('github_runner_frontend_repository', 'copenhagentruckwash/pleno-vue'),
|
||||
'frontend'
|
||||
),
|
||||
'backend' => $this->normalizeGithubRepository(
|
||||
$input['backend_repository'] ?? $input['backend_repo'] ?? $this->coolifyConfigValue('github_runner_backend_repository', 'copenhagentruckwash/api'),
|
||||
'backend'
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
private function normalizeGithubRepository(mixed $value, string $label): string
|
||||
{
|
||||
$repository = trim((string)$value);
|
||||
$repository = preg_replace('#^https://github\.com/#i', '', $repository) ?? $repository;
|
||||
$repository = preg_replace('#^git@github\.com:#i', '', $repository) ?? $repository;
|
||||
$repository = preg_replace('#\.git$#i', '', $repository) ?? $repository;
|
||||
$repository = trim($repository, " \t\n\r\0\x0B/");
|
||||
if (!preg_match('#^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$#', $repository)) {
|
||||
throw new RuntimeException('GitHub ' . $label . ' repository must be in owner/repo format.');
|
||||
}
|
||||
return $repository;
|
||||
}
|
||||
|
||||
private function githubRunnerLabels(mixed $value): array
|
||||
{
|
||||
$raw = trim((string)($value ?? ''));
|
||||
if ($raw === '') {
|
||||
$raw = $this->coolifyConfigValue('github_runner_labels', 'self-hosted,Linux,X64,default');
|
||||
}
|
||||
|
||||
$labels = array_values(array_unique(array_filter(array_map(
|
||||
static fn(string $label): string => trim($label),
|
||||
preg_split('/[,\s]+/', $raw) ?: []
|
||||
))));
|
||||
|
||||
return $labels !== [] ? $labels : ['self-hosted', 'Linux', 'X64', 'default'];
|
||||
}
|
||||
|
||||
private function githubRunnerCount(mixed $value): int
|
||||
{
|
||||
$count = (int)($value ?? 0);
|
||||
if ($count <= 0) {
|
||||
$count = (int)$this->coolifyConfigValue('github_runner_count_per_repo', '1');
|
||||
}
|
||||
return max(1, min(10, $count));
|
||||
}
|
||||
|
||||
private function githubRunnerServiceName(mixed $value): string
|
||||
{
|
||||
$name = strtolower(trim((string)($value ?? 'truckwash-github-runners')));
|
||||
$name = preg_replace('/[^a-z0-9-]+/', '-', $name) ?: '';
|
||||
$name = trim($name, '-') ?: 'truckwash-github-runners';
|
||||
return substr($name, 0, 120);
|
||||
}
|
||||
|
||||
private function githubRunnerToken(array $input): string
|
||||
{
|
||||
$token = trim((string)($input['github_token'] ?? $input['token'] ?? ''));
|
||||
if ($token !== '') {
|
||||
return $token;
|
||||
}
|
||||
|
||||
$envToken = trim((string)(getenv('GITHUB_RUNNER_TOKEN') ?: getenv('GITHUB_TOKEN') ?: ''));
|
||||
if ($envToken !== '') {
|
||||
return $envToken;
|
||||
}
|
||||
|
||||
return replication_secret_box::decrypt($this->coolifyConfigValue('github_runner_token', ''));
|
||||
}
|
||||
|
||||
private function githubRunnerComposeTemplate(array $repositories, array $labels, int $countPerRepo): array
|
||||
{
|
||||
$lines = ['services:'];
|
||||
foreach ($repositories as $key => $repository) {
|
||||
for ($index = 1; $index <= $countPerRepo; $index++) {
|
||||
$service = 'github-runner-' . $key . '-' . $index;
|
||||
$runnerName = 'truckwash-' . $key . '-' . $index;
|
||||
$runnerLabels = array_values(array_unique(array_merge($labels, [$key])));
|
||||
$lines = array_merge($lines, [
|
||||
' ' . $service . ':',
|
||||
' image: myoung34/github-runner:latest',
|
||||
' restart: unless-stopped',
|
||||
' environment:',
|
||||
' REPO_URL: ' . self::yamlScalar('https://github.com/' . $repository),
|
||||
' RUNNER_NAME: ' . self::yamlScalar($runnerName),
|
||||
' RUNNER_SCOPE: repo',
|
||||
' RUNNER_WORKDIR: /tmp/runner/work',
|
||||
' LABELS: ' . self::yamlScalar(implode(',', $runnerLabels)),
|
||||
' EPHEMERAL: "false"',
|
||||
' RUN_AS_ROOT: "true"',
|
||||
' ACCESS_TOKEN: ${GITHUB_RUNNER_TOKEN}',
|
||||
' volumes:',
|
||||
' - /var/run/docker.sock:/var/run/docker.sock',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'compose' => implode("\n", $lines) . "\n",
|
||||
'env' => 'GITHUB_RUNNER_TOKEN=${GITHUB_RUNNER_TOKEN}',
|
||||
];
|
||||
}
|
||||
|
||||
private function githubRunnerServicePayload(array $instance, array $input, string $serviceName, array $template, bool $update): array
|
||||
{
|
||||
$payload = [
|
||||
'name' => $serviceName,
|
||||
'description' => 'Truckwash GitHub self-hosted runners for frontend and backend workflows.',
|
||||
'instant_deploy' => false,
|
||||
'docker_compose_raw' => $this->encodedDockerCompose($template),
|
||||
'force_domain_override' => false,
|
||||
];
|
||||
|
||||
if (!$update) {
|
||||
$payload = array_replace($payload, [
|
||||
'project_uuid' => $this->targetMapping($input, $instance, 'project_uuid'),
|
||||
'environment_name' => $this->targetMapping($input, $instance, 'environment_name') ?: 'production',
|
||||
'environment_uuid' => $this->targetMapping($input, $instance, 'environment_uuid'),
|
||||
'server_uuid' => $this->targetMapping($input, $instance, 'server_uuid'),
|
||||
'destination_uuid' => $this->targetMapping($input, $instance, 'destination_uuid'),
|
||||
]);
|
||||
}
|
||||
|
||||
return array_filter($payload, static fn($value): bool => $value !== null && $value !== '');
|
||||
}
|
||||
|
||||
private static function yamlScalar(string $value): string
|
||||
{
|
||||
return '"' . str_replace(['\\', '"'], ['\\\\', '\\"'], $value) . '"';
|
||||
}
|
||||
|
||||
private function publicLoadBalancerConfig(array $config): array
|
||||
{
|
||||
unset($config['token']);
|
||||
|
||||
@@ -154,6 +154,12 @@ class coolify_schema_bootstrap
|
||||
self::ensureModuleConfigDefault('Coolify', 'hetzner_cloud_api_token', '', 'string');
|
||||
self::ensureModuleConfigDefault('Coolify', 'public_gateway_host', 'api-v2.truckwash.io', 'string');
|
||||
self::ensureModuleConfigDefault('Coolify', 'public_gateway_probe_path', '', 'string');
|
||||
self::ensureModuleConfigDefault('Coolify', 'github_runner_token', '', 'string');
|
||||
self::ensureModuleConfigDefault('Coolify', 'github_runner_service_uuid', '', 'string');
|
||||
self::ensureModuleConfigDefault('Coolify', 'github_runner_frontend_repository', 'copenhagentruckwash/pleno-vue', 'string');
|
||||
self::ensureModuleConfigDefault('Coolify', 'github_runner_backend_repository', 'copenhagentruckwash/api', 'string');
|
||||
self::ensureModuleConfigDefault('Coolify', 'github_runner_labels', 'self-hosted,Linux,X64,default', 'string');
|
||||
self::ensureModuleConfigDefault('Coolify', 'github_runner_count_per_repo', '1', 'int');
|
||||
|
||||
self::ensureDefaultGateway('node1.truckwash.io', '94.130.142.41', 10);
|
||||
self::ensureDefaultGateway('node2.truckwash.io', '65.21.214.30', 20);
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace modules\coolify\config;
|
||||
|
||||
use traits\module_config_variable;
|
||||
|
||||
class coolify_github_runner_backend_repository_c
|
||||
{
|
||||
use module_config_variable;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->setupConfigVariable(
|
||||
'Coolify',
|
||||
'github_runner_backend_repository',
|
||||
'string',
|
||||
false,
|
||||
null,
|
||||
'GitHub backend repository that receives Coolify-managed self-hosted runners.',
|
||||
'copenhagentruckwash/api',
|
||||
false,
|
||||
'copenhagentruckwash/api'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace modules\coolify\config;
|
||||
|
||||
use traits\module_config_variable;
|
||||
|
||||
class coolify_github_runner_count_per_repo_c
|
||||
{
|
||||
use module_config_variable;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->setupConfigVariable(
|
||||
'Coolify',
|
||||
'github_runner_count_per_repo',
|
||||
'int',
|
||||
false,
|
||||
null,
|
||||
'Number of self-hosted GitHub runner containers to deploy per repository.',
|
||||
'1',
|
||||
false,
|
||||
'1'
|
||||
);
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace modules\coolify\config;
|
||||
|
||||
use traits\module_config_variable;
|
||||
|
||||
class coolify_github_runner_frontend_repository_c
|
||||
{
|
||||
use module_config_variable;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->setupConfigVariable(
|
||||
'Coolify',
|
||||
'github_runner_frontend_repository',
|
||||
'string',
|
||||
false,
|
||||
null,
|
||||
'GitHub frontend repository that receives Coolify-managed self-hosted runners.',
|
||||
'copenhagentruckwash/pleno-vue',
|
||||
false,
|
||||
'copenhagentruckwash/pleno-vue'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace modules\coolify\config;
|
||||
|
||||
use traits\module_config_variable;
|
||||
|
||||
class coolify_github_runner_labels_c
|
||||
{
|
||||
use module_config_variable;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->setupConfigVariable(
|
||||
'Coolify',
|
||||
'github_runner_labels',
|
||||
'string',
|
||||
false,
|
||||
null,
|
||||
'Comma-separated GitHub Actions runner labels registered on each Coolify-managed runner.',
|
||||
'self-hosted,Linux,X64,default',
|
||||
false,
|
||||
'self-hosted,Linux,X64,default'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace modules\coolify\config;
|
||||
|
||||
use traits\module_config_variable;
|
||||
|
||||
class coolify_github_runner_service_uuid_c
|
||||
{
|
||||
use module_config_variable;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->setupConfigVariable(
|
||||
'Coolify',
|
||||
'github_runner_service_uuid',
|
||||
'string',
|
||||
false,
|
||||
null,
|
||||
'Coolify service UUID for the managed GitHub self-hosted runner stack.',
|
||||
'abc123...',
|
||||
false,
|
||||
''
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace modules\coolify\config;
|
||||
|
||||
use classes\replication_secret_box;
|
||||
use traits\module_config_variable;
|
||||
|
||||
class coolify_github_runner_token_c
|
||||
{
|
||||
use module_config_variable {
|
||||
setVariableValue as private traitSetVariableValue;
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->setupConfigVariable(
|
||||
'Coolify',
|
||||
'github_runner_token',
|
||||
'string',
|
||||
false,
|
||||
null,
|
||||
'GitHub PAT used to register Coolify-managed self-hosted repository runners.',
|
||||
'github_pat_...',
|
||||
true,
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
public function setVariableValue(mixed $value): void
|
||||
{
|
||||
$value = trim((string)($value ?? ''));
|
||||
if ($value !== '' && !str_starts_with($value, 'twsec:v1:')) {
|
||||
$value = replication_secret_box::encrypt($value);
|
||||
}
|
||||
|
||||
$this->traitSetVariableValue($value);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -9234,6 +9234,47 @@ paths:
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
|
||||
/modules/self-serve/lane/wash/my-active-wash:
|
||||
get:
|
||||
tags:
|
||||
- Modules
|
||||
summary: Get the authenticated customer's active self-serve wash
|
||||
description: |
|
||||
Returns the latest active self-serve wash for the authenticated customer,
|
||||
without requiring the frontend to know or poll a lane id.
|
||||
operationId: getMyActiveSelfServeWash
|
||||
responses:
|
||||
'200':
|
||||
description: Active self-serve wash details resolved
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
lane_id:
|
||||
type: integer
|
||||
status:
|
||||
type: string
|
||||
in_progress:
|
||||
type: boolean
|
||||
elapsed_minutes:
|
||||
type: integer
|
||||
session:
|
||||
type: object
|
||||
nullable: true
|
||||
customer:
|
||||
type: object
|
||||
nullable: true
|
||||
vehicle:
|
||||
type: object
|
||||
nullable: true
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
|
||||
/modules/self-serve/sessions:
|
||||
get:
|
||||
tags:
|
||||
@@ -11176,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:
|
||||
@@ -13471,6 +13552,8 @@ components:
|
||||
additionalProperties: true
|
||||
load_balancer:
|
||||
$ref: '#/components/schemas/SuperuserCoolifyLoadBalancer'
|
||||
github_runners:
|
||||
$ref: '#/components/schemas/SuperuserCoolifyGithubRunnerSummary'
|
||||
meta:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
@@ -13523,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:
|
||||
|
||||
@@ -334,6 +334,23 @@ class moduleSelfServeRoute
|
||||
]
|
||||
);
|
||||
|
||||
/** Modules > Self Serve > Lane > Wash > My active wash */
|
||||
$this->get('/modules/self-serve/lane/wash/my-active-wash', function () {
|
||||
global $response;
|
||||
|
||||
$customer_number = $this->requireMyActiveWashCustomerNumber();
|
||||
$session = $this->findLatestActiveSelfServeSessionForCustomer($customer_number);
|
||||
if (!$session->exists()) {
|
||||
$response->error('No active self-serve wash found.', 404);
|
||||
}
|
||||
|
||||
$response->success($this->buildActiveSelfServeSessionResponse($session));
|
||||
},
|
||||
[
|
||||
'list_own_department_selfserve_vehicle_conditions' => 'View the authenticated customer\'s active self-serve wash',
|
||||
]
|
||||
);
|
||||
|
||||
/** Modules > Self Serve > Sessions */
|
||||
$this->get('/modules/self-serve/sessions', function () {
|
||||
global $response;
|
||||
@@ -1324,6 +1341,188 @@ class moduleSelfServeRoute
|
||||
return null;
|
||||
}
|
||||
|
||||
private function requireMyActiveWashCustomerNumber(): int
|
||||
{
|
||||
global $response;
|
||||
|
||||
$user = (new authentication())->get_user();
|
||||
if (!$user) {
|
||||
$response->error('Authentication failed. Invalid or missing token.', 401);
|
||||
}
|
||||
|
||||
if (!self::hasPermission(self::CUSTOMER_SELFSERVE_PERMISSION)) {
|
||||
$this->emitForbidden([self::CUSTOMER_SELFSERVE_PERMISSION]);
|
||||
}
|
||||
|
||||
$customer_number = $this->resolveEffectiveCustomerNumber();
|
||||
if ($customer_number === null || $customer_number <= 0) {
|
||||
$response->error('No customer number found for authenticated user.', 404);
|
||||
}
|
||||
|
||||
return (int)$customer_number;
|
||||
}
|
||||
|
||||
private function findLatestActiveSelfServeSessionForCustomer(int $customer_number): selfserve_wash_sessions_o
|
||||
{
|
||||
if ($customer_number <= 0) {
|
||||
return new selfserve_wash_sessions_o();
|
||||
}
|
||||
|
||||
$rows = (new selfserve_wash_sessions_o())->getFieldsWhere([
|
||||
'customer_number' => $customer_number,
|
||||
'completed_at' => null,
|
||||
'deleted_at' => null,
|
||||
], ['id', 'status']);
|
||||
|
||||
$active_statuses = $this->activeSelfServeWashSessionStatusValues();
|
||||
$rows = array_values(array_filter(
|
||||
$rows,
|
||||
static fn(array $row): bool => in_array((string)($row['status'] ?? ''), $active_statuses, true)
|
||||
));
|
||||
if ($rows === []) {
|
||||
return new selfserve_wash_sessions_o();
|
||||
}
|
||||
|
||||
usort($rows, static fn(array $a, array $b): int => (int)$b['id'] <=> (int)$a['id']);
|
||||
return (new selfserve_wash_sessions_o())->select((int)$rows[0]['id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int,string>
|
||||
*/
|
||||
private function activeSelfServeWashSessionStatusValues(): array
|
||||
{
|
||||
return array_map(
|
||||
static fn(selfserve_wash_session_status $status): string => $status->value,
|
||||
[
|
||||
selfserve_wash_session_status::MACHINE_RELAY_ENABLED,
|
||||
selfserve_wash_session_status::READY_FOR_MACHINE_START,
|
||||
selfserve_wash_session_status::MACHINE_STARTED,
|
||||
selfserve_wash_session_status::PENDING_QUESTIONS,
|
||||
selfserve_wash_session_status::MACHINE_NOT_ALLOWED,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private function buildActiveSelfServeSessionResponse(selfserve_wash_sessions_o $session): array
|
||||
{
|
||||
$lane_id = (int)$session->lane_id->value();
|
||||
$customer_number = $session->customer_number->value() === null ? null : (int)$session->customer_number->value();
|
||||
$vehicle_id = $session->vehicle_id->value() === null ? null : (int)$session->vehicle_id->value();
|
||||
$session_reg = trim((string)$session->reg->value());
|
||||
if ($session_reg === '') {
|
||||
$session_reg = null;
|
||||
}
|
||||
|
||||
$machine_start_triggered_at = $session->machine_start_triggered_at->value() === null ? null : (string)$session->machine_start_triggered_at->value();
|
||||
$wash_started_at = $session->wash_started_at->value() === null ? null : (string)$session->wash_started_at->value();
|
||||
if ($wash_started_at === null) {
|
||||
$wash_started_at = $machine_start_triggered_at;
|
||||
}
|
||||
|
||||
$machine_relay_enabled = (bool)$session->machine_relay_enabled->value();
|
||||
$included_minutes = null;
|
||||
if ($machine_relay_enabled) {
|
||||
$included_minutes = (int)(new selfserve())->config->machine_wash_minutes_included->getVariableValue();
|
||||
if ($included_minutes < 0) {
|
||||
$included_minutes = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'lane_id' => $lane_id,
|
||||
'status' => (string)$session->status->value(),
|
||||
'in_progress' => true,
|
||||
'elapsed_minutes' => $session->getElapsedMinutes(),
|
||||
'session' => [
|
||||
'id' => (int)$session->id,
|
||||
'status' => (string)$session->status->value(),
|
||||
'department_id' => $session->department_id->value() === null ? null : (int)$session->department_id->value(),
|
||||
'lane_id' => $lane_id,
|
||||
'reg' => $session_reg,
|
||||
'customer_number' => $customer_number,
|
||||
'vehicle_id' => $vehicle_id,
|
||||
'vehicle_type_id' => $session->vehicle_type_id->value() === null ? null : (int)$session->vehicle_type_id->value(),
|
||||
'included_minutes' => $included_minutes ?? 0,
|
||||
'machine_type_id' => $session->machine_type_id->value() === null ? null : (int)$session->machine_type_id->value(),
|
||||
'machine_relay_enabled' => $machine_relay_enabled,
|
||||
'machine_relay_enabled_at' => $session->machine_relay_enabled_at->value() === null ? null : (string)$session->machine_relay_enabled_at->value(),
|
||||
'machine_start_triggered' => (bool)$session->machine_start_triggered->value(),
|
||||
'machine_start_triggered_at' => $machine_start_triggered_at,
|
||||
'wash_started_at' => $wash_started_at,
|
||||
'created_at' => (string)$session->created_at->value(),
|
||||
'updated_at' => $session->updated_at->value() === null ? null : (string)$session->updated_at->value(),
|
||||
],
|
||||
'customer' => $this->buildInProgressWashCustomerPayload($customer_number),
|
||||
'vehicle' => $this->buildInProgressWashVehiclePayload($vehicle_id, $session_reg),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>|null
|
||||
*/
|
||||
private function buildInProgressWashCustomerPayload(?int $customer_number): ?array
|
||||
{
|
||||
if ($customer_number === null || $customer_number <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$customer_obj = (new users_o())->getUserByCustomerNumber($customer_number);
|
||||
if ($customer_obj->exists()) {
|
||||
return [
|
||||
'id' => (int)$customer_obj->id,
|
||||
'customer_number' => $customer_number,
|
||||
'display_name' => $customer_obj->display_name->value() === null ? null : (string)$customer_obj->display_name->value(),
|
||||
'email' => $customer_obj->email->value() === null ? null : (string)$customer_obj->email->value(),
|
||||
'phone_country_code' => $customer_obj->phone_country_code->value() === null ? null : (int)$customer_obj->phone_country_code->value(),
|
||||
'phone' => $customer_obj->phone->value() === null ? null : (string)$customer_obj->phone->value(),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => null,
|
||||
'customer_number' => $customer_number,
|
||||
'display_name' => null,
|
||||
'email' => null,
|
||||
'phone_country_code' => null,
|
||||
'phone' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>|null
|
||||
*/
|
||||
private function buildInProgressWashVehiclePayload(?int $vehicle_id, ?string $reg): ?array
|
||||
{
|
||||
$vehicle_obj = null;
|
||||
if ($vehicle_id !== null && $vehicle_id > 0) {
|
||||
$tmp_vehicle = (new customer_vehicles_o())->select($vehicle_id);
|
||||
if ($tmp_vehicle->exists()) {
|
||||
$vehicle_obj = $tmp_vehicle;
|
||||
}
|
||||
}
|
||||
if ($vehicle_obj === null && $reg !== null && trim($reg) !== '') {
|
||||
$tmp_vehicle = (new customer_vehicles_o())->selectByPlate(trim($reg));
|
||||
if ($tmp_vehicle->exists()) {
|
||||
$vehicle_obj = $tmp_vehicle;
|
||||
}
|
||||
}
|
||||
if ($vehicle_obj === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => (int)$vehicle_obj->id,
|
||||
'customer_id' => (int)$vehicle_obj->customer_id->value(),
|
||||
'type' => (int)$vehicle_obj->type->value(),
|
||||
'reg' => (string)$vehicle_obj->reg->value(),
|
||||
'reference' => $vehicle_obj->reference->value() === null ? null : (string)$vehicle_obj->reference->value(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $status
|
||||
* @param array<string,mixed> $extra
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -10,6 +10,14 @@ it('requires authentication before checking in-progress wash permissions', funct
|
||||
->assertMessage('Authentication failed. Invalid or missing token.');
|
||||
});
|
||||
|
||||
it('requires authentication before checking the current customers active self-serve wash', function (): void {
|
||||
$response = api_client()->get('/modules/self-serve/lane/wash/my-active-wash');
|
||||
|
||||
$response
|
||||
->assertStatus(401)
|
||||
->assertMessage('Authentication failed. Invalid or missing token.');
|
||||
});
|
||||
|
||||
it('reports both elevated and customer self-serve permissions when lane polling is not allowed', function (): void {
|
||||
$session = api_fixtures()->createUserSession([]);
|
||||
|
||||
@@ -26,6 +34,18 @@ it('reports both elevated and customer self-serve permissions when lane polling
|
||||
]);
|
||||
});
|
||||
|
||||
it('requires customer self-serve permission before checking my active wash', function (): void {
|
||||
$session = api_fixtures()->createUserSession([]);
|
||||
|
||||
$response = api_client()->get('/modules/self-serve/lane/wash/my-active-wash', $session['headers']);
|
||||
|
||||
$response
|
||||
->assertStatus(403)
|
||||
->assertMissingPermissions([
|
||||
'list_own_department_selfserve_vehicle_conditions',
|
||||
]);
|
||||
});
|
||||
|
||||
it('allows customer self-serve permission to view their own in-progress wash details', function (): void {
|
||||
$group = api_fixtures()->createGroup([], [
|
||||
'list_own_department_selfserve_vehicle_conditions',
|
||||
@@ -51,6 +71,46 @@ it('allows customer self-serve permission to view their own in-progress wash det
|
||||
->and($response->data()['vehicle']['reg'] ?? null)->toBe($scenario['vehicle']['reg']);
|
||||
});
|
||||
|
||||
it('returns the authenticated customers active self-serve wash without requiring a lane id', function (): void {
|
||||
$group = api_fixtures()->createGroup([], [
|
||||
'list_own_department_selfserve_vehicle_conditions',
|
||||
]);
|
||||
$scenario = api_fixtures()->createSelfServeScenario([
|
||||
'customer' => [
|
||||
'group_id' => $group['id'],
|
||||
],
|
||||
]);
|
||||
$token = api_fixtures()->createAuthToken((int)$scenario['customer']['id']);
|
||||
|
||||
$response = api_client()->get(
|
||||
'/modules/self-serve/lane/wash/my-active-wash',
|
||||
api_fixtures()->bearerHeaders($token)
|
||||
);
|
||||
|
||||
$response
|
||||
->assertStatus(200)
|
||||
->assertSuccess(true);
|
||||
|
||||
expect($response->data()['in_progress'] ?? null)->toBeTrue()
|
||||
->and($response->data()['lane_id'] ?? null)->toBe((int)$scenario['lane']['id'])
|
||||
->and($response->data()['session']['id'] ?? null)->toBe((int)$scenario['session']['id'])
|
||||
->and($response->data()['session']['lane_id'] ?? null)->toBe((int)$scenario['lane']['id'])
|
||||
->and($response->data()['session']['customer_number'] ?? null)->toBe((int)$scenario['customer']['customer_number'])
|
||||
->and($response->data()['vehicle']['reg'] ?? null)->toBe($scenario['vehicle']['reg']);
|
||||
});
|
||||
|
||||
it('returns 404 when the authenticated customer has no active self-serve wash', function (): void {
|
||||
$session = api_fixtures()->createUserSession([
|
||||
'list_own_department_selfserve_vehicle_conditions',
|
||||
]);
|
||||
|
||||
$response = api_client()->get('/modules/self-serve/lane/wash/my-active-wash', $session['headers']);
|
||||
|
||||
$response
|
||||
->assertStatus(404)
|
||||
->assertMessage('No active self-serve wash found.');
|
||||
});
|
||||
|
||||
it('redacts another customers in-progress wash from customer self-serve lane polling', function (): void {
|
||||
$scenario = api_fixtures()->createSelfServeScenario();
|
||||
$otherSession = api_fixtures()->createUserSession([
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -52,6 +52,7 @@ it('documents self-serve machine type, eligibility, summary, and webhook endpoin
|
||||
expect($content)->toContain('/relay/button/press/post:');
|
||||
expect($content)->toContain('/relay/machine/on/post:');
|
||||
expect($content)->toContain('/modules/self-serve/lane/wash/in-progress:');
|
||||
expect($content)->toContain('/modules/self-serve/lane/wash/my-active-wash:');
|
||||
expect($content)->toContain('/modules/self-serve/lane/relay/machine/status:');
|
||||
expect($content)->toContain('/modules/self-serve/lane/relay/machine/set:');
|
||||
expect($content)->toContain('/modules/self-serve/lane/gate/open:');
|
||||
|
||||
@@ -286,9 +286,12 @@ it('wires in-progress self-serve wash details endpoint', function (): void {
|
||||
|
||||
expect($moduleSelfServeRoute)->not->toBeFalse();
|
||||
expect($moduleSelfServeRoute)->toContain('/modules/self-serve/lane/wash/in-progress');
|
||||
expect($moduleSelfServeRoute)->toContain('/modules/self-serve/lane/wash/my-active-wash');
|
||||
expect($moduleSelfServeRoute)->toContain('modules_selfserve_lane_wash_in_progress_view');
|
||||
expect($moduleSelfServeRoute)->toContain('list_own_department_selfserve_vehicle_conditions');
|
||||
expect($moduleSelfServeRoute)->toContain('requireInProgressWashDetailsAccess($lane_id)');
|
||||
expect($moduleSelfServeRoute)->toContain('findLatestActiveSelfServeSessionForCustomer($customer_number)');
|
||||
expect($moduleSelfServeRoute)->toContain('buildActiveSelfServeSessionResponse($session)');
|
||||
expect($moduleSelfServeRoute)->toContain('(new department_lanes_o())->select($lane_id)');
|
||||
expect($moduleSelfServeRoute)->toContain('self::requireDepartmentAccess((string)$department_lane->department->value())');
|
||||
expect($moduleSelfServeRoute)->toContain('scopeInProgressWashResponseForCustomer');
|
||||
|
||||
Reference in New Issue
Block a user