mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-17 18:14:03 +00:00
Resolve remaining merge conflicts
This commit is contained in:
commit
aa445b4a6c
792 files changed
+58746
-7239
No files matched your search
@@ -14,7 +14,7 @@ class CleanupUnreachableServers extends Command
|
||||
public function handle()
|
||||
{
|
||||
echo "Running unreachable server cleanup...\n";
|
||||
$servers = Server::where('unreachable_count', 3)->where('unreachable_notification_sent', true)->where('updated_at', '<', now()->subDays(7))->get();
|
||||
$servers = Server::where('unreachable_count', '>=', 3)->where('unreachable_notification_sent', true)->where('updated_at', '<', now()->subDays(7))->get();
|
||||
if ($servers->count() > 0) {
|
||||
foreach ($servers as $server) {
|
||||
echo "Cleanup unreachable server ($server->id) with name $server->name";
|
||||
|
||||
@@ -36,7 +36,14 @@ class SyncStripeSubscriptions extends Command
|
||||
$this->newLine();
|
||||
|
||||
$job = new SyncStripeSubscriptionsJob($fix);
|
||||
$result = $job->handle();
|
||||
$fetched = 0;
|
||||
$result = $job->handle(function (int $count) use (&$fetched): void {
|
||||
$fetched = $count;
|
||||
$this->output->write("\r Fetching subscriptions from Stripe... {$fetched}");
|
||||
});
|
||||
if ($fetched > 0) {
|
||||
$this->output->write("\r".str_repeat(' ', 60)."\r");
|
||||
}
|
||||
|
||||
if (isset($result['error'])) {
|
||||
$this->error($result['error']);
|
||||
@@ -68,6 +75,19 @@ class SyncStripeSubscriptions extends Command
|
||||
$this->info('No discrepancies found. All subscriptions are in sync.');
|
||||
}
|
||||
|
||||
if (count($result['resubscribed']) > 0) {
|
||||
$this->newLine();
|
||||
$this->warn('Resubscribed users (same email, different customer): '.count($result['resubscribed']));
|
||||
$this->newLine();
|
||||
|
||||
foreach ($result['resubscribed'] as $resub) {
|
||||
$this->line(" - Team ID: {$resub['team_id']} | Email: {$resub['email']}");
|
||||
$this->line(" Old: {$resub['old_stripe_subscription_id']} (cus: {$resub['old_stripe_customer_id']})");
|
||||
$this->line(" New: {$resub['new_stripe_subscription_id']} (cus: {$resub['new_stripe_customer_id']}) [{$resub['new_status']}]");
|
||||
$this->newLine();
|
||||
}
|
||||
}
|
||||
|
||||
if (count($result['errors']) > 0) {
|
||||
$this->newLine();
|
||||
$this->error('Errors encountered: '.count($result['errors']));
|
||||
|
||||
@@ -30,32 +30,32 @@ class Dev extends Command
|
||||
// Generate APP_KEY if not exists
|
||||
|
||||
if (empty(config('app.key'))) {
|
||||
echo "Generating APP_KEY.\n";
|
||||
echo " INFO Generating APP_KEY.\n";
|
||||
Artisan::call('key:generate');
|
||||
}
|
||||
|
||||
// Generate STORAGE link if not exists
|
||||
if (! file_exists(public_path('storage'))) {
|
||||
echo "Generating STORAGE link.\n";
|
||||
echo " INFO Generating storage link.\n";
|
||||
Artisan::call('storage:link');
|
||||
}
|
||||
|
||||
// Seed database if it's empty
|
||||
$settings = InstanceSettings::find(0);
|
||||
if (! $settings) {
|
||||
echo "Initializing instance, seeding database.\n";
|
||||
echo " INFO Initializing instance, seeding database.\n";
|
||||
Artisan::call('migrate --seed');
|
||||
} else {
|
||||
echo "Instance already initialized.\n";
|
||||
echo " INFO Instance already initialized.\n";
|
||||
}
|
||||
|
||||
// Clean up stuck jobs and stale locks on development startup
|
||||
try {
|
||||
echo "Cleaning up Redis (stuck jobs and stale locks)...\n";
|
||||
echo " INFO Cleaning up Redis (stuck jobs and stale locks)...\n";
|
||||
Artisan::call('cleanup:redis', ['--restart' => true, '--clear-locks' => true]);
|
||||
echo "Redis cleanup completed.\n";
|
||||
echo " INFO Redis cleanup completed.\n";
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleanup:redis: {$e->getMessage()}\n";
|
||||
echo " ERROR Redis cleanup failed: {$e->getMessage()}\n";
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -66,10 +66,10 @@ class Dev extends Command
|
||||
]);
|
||||
|
||||
if ($updatedTaskCount > 0) {
|
||||
echo "Marked {$updatedTaskCount} stuck scheduled task executions as failed\n";
|
||||
echo " INFO Marked {$updatedTaskCount} stuck scheduled task executions as failed.\n";
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Could not cleanup stuck scheduled task executions: {$e->getMessage()}\n";
|
||||
echo " ERROR Could not clean up stuck scheduled task executions: {$e->getMessage()}\n";
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -80,10 +80,10 @@ class Dev extends Command
|
||||
]);
|
||||
|
||||
if ($updatedBackupCount > 0) {
|
||||
echo "Marked {$updatedBackupCount} stuck database backup executions as failed\n";
|
||||
echo " INFO Marked {$updatedBackupCount} stuck database backup executions as failed.\n";
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Could not cleanup stuck database backup executions: {$e->getMessage()}\n";
|
||||
echo " ERROR Could not clean up stuck database backup executions: {$e->getMessage()}\n";
|
||||
}
|
||||
|
||||
CheckHelperImageJob::dispatch();
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class GenerateTestingSchema extends Command
|
||||
{
|
||||
protected $signature = 'schema:generate-testing {--connection=pgsql : The database connection to read from}';
|
||||
|
||||
protected $description = 'Generate SQLite testing schema from the PostgreSQL database';
|
||||
|
||||
private array $typeMap = [
|
||||
'/\bbigint\b/' => 'INTEGER',
|
||||
'/\binteger\b/' => 'INTEGER',
|
||||
'/\bsmallint\b/' => 'INTEGER',
|
||||
'/\bboolean\b/' => 'INTEGER',
|
||||
'/character varying\(\d+\)/' => 'TEXT',
|
||||
'/timestamp\(\d+\) without time zone/' => 'TEXT',
|
||||
'/timestamp\(\d+\) with time zone/' => 'TEXT',
|
||||
'/\bjsonb\b/' => 'TEXT',
|
||||
'/\bjson\b/' => 'TEXT',
|
||||
'/\buuid\b/' => 'TEXT',
|
||||
'/double precision/' => 'REAL',
|
||||
'/numeric\(\d+,\d+\)/' => 'REAL',
|
||||
'/\bdate\b/' => 'TEXT',
|
||||
];
|
||||
|
||||
private array $castRemovals = [
|
||||
'::character varying',
|
||||
'::text',
|
||||
'::integer',
|
||||
'::boolean',
|
||||
'::timestamp without time zone',
|
||||
'::timestamp with time zone',
|
||||
'::numeric',
|
||||
];
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$connection = $this->option('connection');
|
||||
|
||||
if (DB::connection($connection)->getDriverName() !== 'pgsql') {
|
||||
$this->error("Connection '{$connection}' is not PostgreSQL.");
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$this->info('Reading schema from PostgreSQL...');
|
||||
|
||||
$tables = $this->getTables($connection);
|
||||
$lastMigration = DB::connection($connection)
|
||||
->table('migrations')
|
||||
->orderByDesc('id')
|
||||
->value('migration');
|
||||
|
||||
$output = [];
|
||||
$output[] = '-- Generated by: php artisan schema:generate-testing';
|
||||
$output[] = '-- Date: '.now()->format('Y-m-d H:i:s');
|
||||
$output[] = '-- Last migration: '.($lastMigration ?? 'none');
|
||||
$output[] = '';
|
||||
|
||||
foreach ($tables as $table) {
|
||||
$columns = $this->getColumns($connection, $table);
|
||||
$output[] = $this->generateCreateTable($table, $columns);
|
||||
}
|
||||
|
||||
$indexes = $this->getIndexes($connection, $tables);
|
||||
foreach ($indexes as $index) {
|
||||
$output[] = $index;
|
||||
}
|
||||
|
||||
$output[] = '';
|
||||
$output[] = '-- Migration records';
|
||||
|
||||
$migrations = DB::connection($connection)->table('migrations')->orderBy('id')->get();
|
||||
foreach ($migrations as $m) {
|
||||
$migration = str_replace("'", "''", $m->migration);
|
||||
$output[] = "INSERT INTO \"migrations\" (\"id\", \"migration\", \"batch\") VALUES ({$m->id}, '{$migration}', {$m->batch});";
|
||||
}
|
||||
|
||||
$path = database_path('schema/testing-schema.sql');
|
||||
|
||||
if (! is_dir(dirname($path))) {
|
||||
mkdir(dirname($path), 0755, true);
|
||||
}
|
||||
|
||||
file_put_contents($path, implode("\n", $output)."\n");
|
||||
|
||||
$this->info("Schema written to {$path}");
|
||||
$this->info(count($tables).' tables, '.count($migrations).' migration records.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function getTables(string $connection): array
|
||||
{
|
||||
return collect(DB::connection($connection)->select(
|
||||
"SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename"
|
||||
))->pluck('tablename')->toArray();
|
||||
}
|
||||
|
||||
private function getColumns(string $connection, string $table): array
|
||||
{
|
||||
return DB::connection($connection)->select(
|
||||
"SELECT column_name, data_type, character_maximum_length, column_default,
|
||||
is_nullable, udt_name, numeric_precision, numeric_scale, datetime_precision
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public' AND table_name = ?
|
||||
ORDER BY ordinal_position",
|
||||
[$table]
|
||||
);
|
||||
}
|
||||
|
||||
private function generateCreateTable(string $table, array $columns): string
|
||||
{
|
||||
$lines = [];
|
||||
|
||||
foreach ($columns as $col) {
|
||||
$lines[] = ' '.$this->generateColumnDef($table, $col);
|
||||
}
|
||||
|
||||
return "CREATE TABLE IF NOT EXISTS \"{$table}\" (\n".implode(",\n", $lines)."\n);\n";
|
||||
}
|
||||
|
||||
private function generateColumnDef(string $table, object $col): string
|
||||
{
|
||||
$name = $col->column_name;
|
||||
$sqliteType = $this->convertType($col);
|
||||
|
||||
// Auto-increment primary key for id columns
|
||||
if ($name === 'id' && $sqliteType === 'INTEGER' && $col->is_nullable === 'NO' && str_contains((string) $col->column_default, 'nextval')) {
|
||||
return "\"{$name}\" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL";
|
||||
}
|
||||
|
||||
$parts = ["\"{$name}\"", $sqliteType];
|
||||
|
||||
// Default value
|
||||
$default = $col->column_default;
|
||||
if ($default !== null && ! str_contains($default, 'nextval')) {
|
||||
$default = $this->cleanDefault($default);
|
||||
$parts[] = "DEFAULT {$default}";
|
||||
}
|
||||
|
||||
// NOT NULL
|
||||
if ($col->is_nullable === 'NO') {
|
||||
$parts[] = 'NOT NULL';
|
||||
}
|
||||
|
||||
return implode(' ', $parts);
|
||||
}
|
||||
|
||||
private function convertType(object $col): string
|
||||
{
|
||||
$pgType = $col->data_type;
|
||||
|
||||
return match (true) {
|
||||
in_array($pgType, ['bigint', 'integer', 'smallint']) => 'INTEGER',
|
||||
$pgType === 'boolean' => 'INTEGER',
|
||||
in_array($pgType, ['character varying', 'text', 'USER-DEFINED']) => 'TEXT',
|
||||
str_contains($pgType, 'timestamp') => 'TEXT',
|
||||
in_array($pgType, ['json', 'jsonb']) => 'TEXT',
|
||||
$pgType === 'uuid' => 'TEXT',
|
||||
$pgType === 'double precision' => 'REAL',
|
||||
$pgType === 'numeric' => 'REAL',
|
||||
$pgType === 'date' => 'TEXT',
|
||||
default => 'TEXT',
|
||||
};
|
||||
}
|
||||
|
||||
private function cleanDefault(string $default): string
|
||||
{
|
||||
foreach ($this->castRemovals as $cast) {
|
||||
$default = str_replace($cast, '', $default);
|
||||
}
|
||||
|
||||
// Remove array type casts like ::text[]
|
||||
$default = preg_replace('/::[\w\s]+(\[\])?/', '', $default);
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
private function getIndexes(string $connection, array $tables): array
|
||||
{
|
||||
$results = [];
|
||||
|
||||
$indexes = DB::connection($connection)->select(
|
||||
"SELECT indexname, tablename, indexdef FROM pg_indexes
|
||||
WHERE schemaname = 'public'
|
||||
ORDER BY tablename, indexname"
|
||||
);
|
||||
|
||||
foreach ($indexes as $idx) {
|
||||
$def = $idx->indexdef;
|
||||
|
||||
// Skip primary key indexes
|
||||
if (str_contains($def, '_pkey')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip PG-specific indexes (GIN, GIST, expression indexes)
|
||||
if (preg_match('/USING (gin|gist)/i', $def)) {
|
||||
continue;
|
||||
}
|
||||
if (str_contains($def, '->>') || str_contains($def, '::')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Convert to SQLite-compatible CREATE INDEX
|
||||
$unique = str_contains($def, 'UNIQUE') ? 'UNIQUE ' : '';
|
||||
|
||||
// Extract columns from the index definition
|
||||
if (preg_match('/\((.+)\)$/', $def, $m)) {
|
||||
$cols = $m[1];
|
||||
$results[] = "CREATE {$unique}INDEX IF NOT EXISTS \"{$idx->indexname}\" ON \"{$idx->tablename}\" ({$cols});";
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class Horizon extends Command
|
||||
{
|
||||
protected $signature = 'start:horizon';
|
||||
|
||||
protected $description = 'Start Horizon';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
if (config('constants.horizon.is_horizon_enabled')) {
|
||||
$this->info('Horizon is enabled on this server.');
|
||||
$this->call('horizon');
|
||||
exit(0);
|
||||
} else {
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -212,18 +212,19 @@ class Init extends Command
|
||||
$removeNetworks = $allNetworks->diff($networks);
|
||||
$commands = collect();
|
||||
foreach ($removeNetworks as $network) {
|
||||
$out = instant_remote_process(["docker network inspect -f json $network | jq '.[].Containers | if . == {} then null else . end'"], $server, false);
|
||||
$safe = escapeshellarg($network);
|
||||
$out = instant_remote_process(["docker network inspect -f json {$safe} | jq '.[].Containers | if . == {} then null else . end'"], $server, false);
|
||||
if (empty($out)) {
|
||||
$commands->push("docker network disconnect $network coolify-proxy >/dev/null 2>&1 || true");
|
||||
$commands->push("docker network rm $network >/dev/null 2>&1 || true");
|
||||
$commands->push("docker network disconnect {$safe} coolify-proxy >/dev/null 2>&1 || true");
|
||||
$commands->push("docker network rm {$safe} >/dev/null 2>&1 || true");
|
||||
} else {
|
||||
$data = collect(json_decode($out, true));
|
||||
if ($data->count() === 1) {
|
||||
// If only coolify-proxy itself is connected to that network (it should not be possible, but who knows)
|
||||
$isCoolifyProxyItself = data_get($data->first(), 'Name') === 'coolify-proxy';
|
||||
if ($isCoolifyProxyItself) {
|
||||
$commands->push("docker network disconnect $network coolify-proxy >/dev/null 2>&1 || true");
|
||||
$commands->push("docker network rm $network >/dev/null 2>&1 || true");
|
||||
$commands->push("docker network disconnect {$safe} coolify-proxy >/dev/null 2>&1 || true");
|
||||
$commands->push("docker network rm {$safe} >/dev/null 2>&1 || true");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\DockerCleanupExecution;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use App\Models\ScheduledTask;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class ScheduledJobDiagnostics extends Command
|
||||
{
|
||||
protected $signature = 'scheduled:diagnostics
|
||||
{--type=all : Type to inspect: docker-cleanup, backups, tasks, server-jobs, all}
|
||||
{--server= : Filter by server ID}';
|
||||
|
||||
protected $description = 'Inspect dedup cache state and scheduling decisions for all scheduled jobs';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$type = $this->option('type');
|
||||
$serverFilter = $this->option('server');
|
||||
|
||||
$this->outputHeartbeat();
|
||||
|
||||
if (in_array($type, ['all', 'docker-cleanup'])) {
|
||||
$this->inspectDockerCleanups($serverFilter);
|
||||
}
|
||||
|
||||
if (in_array($type, ['all', 'backups'])) {
|
||||
$this->inspectBackups();
|
||||
}
|
||||
|
||||
if (in_array($type, ['all', 'tasks'])) {
|
||||
$this->inspectTasks();
|
||||
}
|
||||
|
||||
if (in_array($type, ['all', 'server-jobs'])) {
|
||||
$this->inspectServerJobs($serverFilter);
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function outputHeartbeat(): void
|
||||
{
|
||||
$heartbeat = Cache::get('scheduled-job-manager:heartbeat');
|
||||
if ($heartbeat) {
|
||||
$age = Carbon::parse($heartbeat)->diffForHumans();
|
||||
$this->info("Scheduler heartbeat: {$heartbeat} ({$age})");
|
||||
} else {
|
||||
$this->error('Scheduler heartbeat: MISSING — ScheduledJobManager may not be running');
|
||||
}
|
||||
$this->newLine();
|
||||
}
|
||||
|
||||
private function inspectDockerCleanups(?string $serverFilter): void
|
||||
{
|
||||
$this->info('=== Docker Cleanup Jobs ===');
|
||||
|
||||
$servers = $this->getServers($serverFilter);
|
||||
|
||||
$rows = [];
|
||||
foreach ($servers as $server) {
|
||||
$frequency = data_get($server->settings, 'docker_cleanup_frequency', '0 * * * *');
|
||||
if (isset(VALID_CRON_STRINGS[$frequency])) {
|
||||
$frequency = VALID_CRON_STRINGS[$frequency];
|
||||
}
|
||||
|
||||
$dedupKey = "docker-cleanup:{$server->id}";
|
||||
$cacheValue = Cache::get($dedupKey);
|
||||
$timezone = data_get($server->settings, 'server_timezone', config('app.timezone'));
|
||||
|
||||
if (validate_timezone($timezone) === false) {
|
||||
$timezone = config('app.timezone');
|
||||
}
|
||||
|
||||
$wouldFire = shouldRunCronNow($frequency, $timezone, $dedupKey);
|
||||
|
||||
$lastExecution = DockerCleanupExecution::where('server_id', $server->id)
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
$rows[] = [
|
||||
$server->id,
|
||||
$server->name,
|
||||
$timezone,
|
||||
$frequency,
|
||||
$dedupKey,
|
||||
$cacheValue ?? '<missing>',
|
||||
$wouldFire ? 'YES' : 'no',
|
||||
$lastExecution ? $lastExecution->status.' @ '.$lastExecution->created_at : 'never',
|
||||
];
|
||||
}
|
||||
|
||||
$this->table(
|
||||
['ID', 'Server', 'TZ', 'Frequency', 'Dedup Key', 'Cache Value', 'Would Fire', 'Last Execution'],
|
||||
$rows
|
||||
);
|
||||
$this->newLine();
|
||||
}
|
||||
|
||||
private function inspectBackups(): void
|
||||
{
|
||||
$this->info('=== Scheduled Backups ===');
|
||||
|
||||
$backups = ScheduledDatabaseBackup::with(['database'])
|
||||
->where('enabled', true)
|
||||
->get();
|
||||
|
||||
$rows = [];
|
||||
foreach ($backups as $backup) {
|
||||
$server = $backup->server();
|
||||
$frequency = $backup->frequency;
|
||||
if (isset(VALID_CRON_STRINGS[$frequency])) {
|
||||
$frequency = VALID_CRON_STRINGS[$frequency];
|
||||
}
|
||||
|
||||
$dedupKey = "scheduled-backup:{$backup->id}";
|
||||
$cacheValue = Cache::get($dedupKey);
|
||||
$timezone = $server ? data_get($server->settings, 'server_timezone', config('app.timezone')) : config('app.timezone');
|
||||
|
||||
if (validate_timezone($timezone) === false) {
|
||||
$timezone = config('app.timezone');
|
||||
}
|
||||
|
||||
$wouldFire = shouldRunCronNow($frequency, $timezone, $dedupKey);
|
||||
|
||||
$rows[] = [
|
||||
$backup->id,
|
||||
$backup->database_type ?? 'unknown',
|
||||
$server?->name ?? 'N/A',
|
||||
$frequency,
|
||||
$cacheValue ?? '<missing>',
|
||||
$wouldFire ? 'YES' : 'no',
|
||||
];
|
||||
}
|
||||
|
||||
$this->table(
|
||||
['Backup ID', 'DB Type', 'Server', 'Frequency', 'Cache Value', 'Would Fire'],
|
||||
$rows
|
||||
);
|
||||
$this->newLine();
|
||||
}
|
||||
|
||||
private function inspectTasks(): void
|
||||
{
|
||||
$this->info('=== Scheduled Tasks ===');
|
||||
|
||||
$tasks = ScheduledTask::with(['service', 'application'])
|
||||
->where('enabled', true)
|
||||
->get();
|
||||
|
||||
$rows = [];
|
||||
foreach ($tasks as $task) {
|
||||
$server = $task->server();
|
||||
$frequency = $task->frequency;
|
||||
if (isset(VALID_CRON_STRINGS[$frequency])) {
|
||||
$frequency = VALID_CRON_STRINGS[$frequency];
|
||||
}
|
||||
|
||||
$dedupKey = "scheduled-task:{$task->id}";
|
||||
$cacheValue = Cache::get($dedupKey);
|
||||
$timezone = $server ? data_get($server->settings, 'server_timezone', config('app.timezone')) : config('app.timezone');
|
||||
|
||||
if (validate_timezone($timezone) === false) {
|
||||
$timezone = config('app.timezone');
|
||||
}
|
||||
|
||||
$wouldFire = shouldRunCronNow($frequency, $timezone, $dedupKey);
|
||||
|
||||
$rows[] = [
|
||||
$task->id,
|
||||
$task->name,
|
||||
$server?->name ?? 'N/A',
|
||||
$frequency,
|
||||
$cacheValue ?? '<missing>',
|
||||
$wouldFire ? 'YES' : 'no',
|
||||
];
|
||||
}
|
||||
|
||||
$this->table(
|
||||
['Task ID', 'Name', 'Server', 'Frequency', 'Cache Value', 'Would Fire'],
|
||||
$rows
|
||||
);
|
||||
$this->newLine();
|
||||
}
|
||||
|
||||
private function inspectServerJobs(?string $serverFilter): void
|
||||
{
|
||||
$this->info('=== Server Manager Jobs ===');
|
||||
|
||||
$servers = $this->getServers($serverFilter);
|
||||
|
||||
$rows = [];
|
||||
foreach ($servers as $server) {
|
||||
$timezone = data_get($server->settings, 'server_timezone', config('app.timezone'));
|
||||
if (validate_timezone($timezone) === false) {
|
||||
$timezone = config('app.timezone');
|
||||
}
|
||||
|
||||
$dedupKeys = [
|
||||
"sentinel-restart:{$server->id}" => '0 0 * * *',
|
||||
"server-patch-check:{$server->id}" => '0 0 * * 0',
|
||||
"server-check:{$server->id}" => isCloud() ? '*/5 * * * *' : '* * * * *',
|
||||
"server-storage-check:{$server->id}" => data_get($server->settings, 'server_disk_usage_check_frequency', '0 23 * * *'),
|
||||
];
|
||||
|
||||
foreach ($dedupKeys as $dedupKey => $frequency) {
|
||||
if (isset(VALID_CRON_STRINGS[$frequency])) {
|
||||
$frequency = VALID_CRON_STRINGS[$frequency];
|
||||
}
|
||||
|
||||
$cacheValue = Cache::get($dedupKey);
|
||||
$wouldFire = shouldRunCronNow($frequency, $timezone, $dedupKey);
|
||||
|
||||
$rows[] = [
|
||||
$server->id,
|
||||
$server->name,
|
||||
$dedupKey,
|
||||
$frequency,
|
||||
$cacheValue ?? '<missing>',
|
||||
$wouldFire ? 'YES' : 'no',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$this->table(
|
||||
['Server ID', 'Server', 'Dedup Key', 'Frequency', 'Cache Value', 'Would Fire'],
|
||||
$rows
|
||||
);
|
||||
$this->newLine();
|
||||
}
|
||||
|
||||
private function getServers(?string $serverFilter): \Illuminate\Support\Collection
|
||||
{
|
||||
$query = Server::with('settings')->where('ip', '!=', '1.2.3.4');
|
||||
|
||||
if ($serverFilter) {
|
||||
$query->where('id', $serverFilter);
|
||||
}
|
||||
|
||||
if (isCloud()) {
|
||||
$servers = $query->whereRelation('team.subscription', 'stripe_invoice_paid', true)->get();
|
||||
$own = Team::find(0)?->servers()->with('settings')->get() ?? collect();
|
||||
|
||||
return $servers->merge($own);
|
||||
}
|
||||
|
||||
return $query->get();
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class Scheduler extends Command
|
||||
{
|
||||
protected $signature = 'start:scheduler';
|
||||
|
||||
protected $description = 'Start Scheduler';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
if (config('constants.horizon.is_scheduler_enabled')) {
|
||||
$this->info('Scheduler is enabled on this server.');
|
||||
$this->call('schedule:work');
|
||||
exit(0);
|
||||
} else {
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -363,6 +363,162 @@ class SyncBunny extends Command
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync install.sh, docker-compose, and env files to GitHub repository via PR
|
||||
*/
|
||||
private function syncFilesToGitHubRepo(array $files, bool $nightly = false): bool
|
||||
{
|
||||
$envLabel = $nightly ? 'NIGHTLY' : 'PRODUCTION';
|
||||
$this->info("Syncing $envLabel files to GitHub repository...");
|
||||
try {
|
||||
$timestamp = time();
|
||||
$tmpDir = sys_get_temp_dir().'/coolify-cdn-files-'.$timestamp;
|
||||
$branchName = 'update-files-'.$timestamp;
|
||||
|
||||
// Clone the repository
|
||||
$this->info('Cloning coolify-cdn repository...');
|
||||
$output = [];
|
||||
exec('gh repo clone coollabsio/coolify-cdn '.escapeshellarg($tmpDir).' 2>&1', $output, $returnCode);
|
||||
if ($returnCode !== 0) {
|
||||
$this->error('Failed to clone repository: '.implode("\n", $output));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create feature branch
|
||||
$this->info('Creating feature branch...');
|
||||
$output = [];
|
||||
exec('cd '.escapeshellarg($tmpDir).' && git checkout -b '.escapeshellarg($branchName).' 2>&1', $output, $returnCode);
|
||||
if ($returnCode !== 0) {
|
||||
$this->error('Failed to create branch: '.implode("\n", $output));
|
||||
exec('rm -rf '.escapeshellarg($tmpDir));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Copy each file to its target path in the CDN repo
|
||||
$copiedFiles = [];
|
||||
foreach ($files as $sourceFile => $targetPath) {
|
||||
if (! file_exists($sourceFile)) {
|
||||
$this->warn("Source file not found, skipping: $sourceFile");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$destPath = "$tmpDir/$targetPath";
|
||||
$destDir = dirname($destPath);
|
||||
|
||||
if (! is_dir($destDir)) {
|
||||
if (! mkdir($destDir, 0755, true)) {
|
||||
$this->error("Failed to create directory: $destDir");
|
||||
exec('rm -rf '.escapeshellarg($tmpDir));
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (copy($sourceFile, $destPath) === false) {
|
||||
$this->error("Failed to copy $sourceFile to $destPath");
|
||||
exec('rm -rf '.escapeshellarg($tmpDir));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$copiedFiles[] = $targetPath;
|
||||
$this->info("Copied: $targetPath");
|
||||
}
|
||||
|
||||
if (empty($copiedFiles)) {
|
||||
$this->warn('No files were copied. Nothing to commit.');
|
||||
exec('rm -rf '.escapeshellarg($tmpDir));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Stage all copied files
|
||||
$this->info('Staging changes...');
|
||||
$output = [];
|
||||
$stageCmd = 'cd '.escapeshellarg($tmpDir).' && git add '.implode(' ', array_map('escapeshellarg', $copiedFiles)).' 2>&1';
|
||||
exec($stageCmd, $output, $returnCode);
|
||||
if ($returnCode !== 0) {
|
||||
$this->error('Failed to stage changes: '.implode("\n", $output));
|
||||
exec('rm -rf '.escapeshellarg($tmpDir));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for changes
|
||||
$this->info('Checking for changes...');
|
||||
$statusOutput = [];
|
||||
exec('cd '.escapeshellarg($tmpDir).' && git status --porcelain 2>&1', $statusOutput, $returnCode);
|
||||
if ($returnCode !== 0) {
|
||||
$this->error('Failed to check repository status: '.implode("\n", $statusOutput));
|
||||
exec('rm -rf '.escapeshellarg($tmpDir));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (empty(array_filter($statusOutput))) {
|
||||
$this->info('All files are already up to date. No changes to commit.');
|
||||
exec('rm -rf '.escapeshellarg($tmpDir));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Commit changes
|
||||
$commitMessage = "Update $envLabel files (install.sh, docker-compose, env) - ".date('Y-m-d H:i:s');
|
||||
$output = [];
|
||||
exec('cd '.escapeshellarg($tmpDir).' && git commit -m '.escapeshellarg($commitMessage).' 2>&1', $output, $returnCode);
|
||||
if ($returnCode !== 0) {
|
||||
$this->error('Failed to commit changes: '.implode("\n", $output));
|
||||
exec('rm -rf '.escapeshellarg($tmpDir));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Push to remote
|
||||
$this->info('Pushing branch to remote...');
|
||||
$output = [];
|
||||
exec('cd '.escapeshellarg($tmpDir).' && git push origin '.escapeshellarg($branchName).' 2>&1', $output, $returnCode);
|
||||
if ($returnCode !== 0) {
|
||||
$this->error('Failed to push branch: '.implode("\n", $output));
|
||||
exec('rm -rf '.escapeshellarg($tmpDir));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create pull request
|
||||
$this->info('Creating pull request...');
|
||||
$prTitle = "Update $envLabel files - ".date('Y-m-d H:i:s');
|
||||
$fileList = implode("\n- ", $copiedFiles);
|
||||
$prBody = "Automated update of $envLabel files:\n- $fileList";
|
||||
$prCommand = 'gh pr create --repo coollabsio/coolify-cdn --title '.escapeshellarg($prTitle).' --body '.escapeshellarg($prBody).' --base main --head '.escapeshellarg($branchName).' 2>&1';
|
||||
$output = [];
|
||||
exec($prCommand, $output, $returnCode);
|
||||
|
||||
// Clean up
|
||||
exec('rm -rf '.escapeshellarg($tmpDir));
|
||||
|
||||
if ($returnCode !== 0) {
|
||||
$this->error('Failed to create PR: '.implode("\n", $output));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->info('Pull request created successfully!');
|
||||
if (! empty($output)) {
|
||||
$this->info('PR URL: '.implode("\n", $output));
|
||||
}
|
||||
$this->info('Files synced: '.count($copiedFiles));
|
||||
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
$this->error('Error syncing files to GitHub: '.$e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync versions.json to GitHub repository via PR
|
||||
*/
|
||||
@@ -581,11 +737,130 @@ class SyncBunny extends Command
|
||||
$versions_location = "$parent_dir/other/nightly/$versions";
|
||||
}
|
||||
if (! $only_template && ! $only_version && ! $only_github_releases && ! $only_github_versions) {
|
||||
$envLabel = $nightly ? 'NIGHTLY' : 'PRODUCTION';
|
||||
$this->info("About to sync $envLabel files to BunnyCDN and create a GitHub PR for coolify-cdn.");
|
||||
$this->newLine();
|
||||
|
||||
// Build file mapping for diff
|
||||
if ($nightly) {
|
||||
$this->info('About to sync files NIGHTLY (docker-compose.prod.yaml, upgrade.sh, install.sh, etc) to BunnyCDN.');
|
||||
$fileMapping = [
|
||||
$compose_file_location => 'docker/nightly/docker-compose.yml',
|
||||
$compose_file_prod_location => 'docker/nightly/docker-compose.prod.yml',
|
||||
$production_env_location => 'environment/nightly/.env.production',
|
||||
$upgrade_script_location => 'scripts/nightly/upgrade.sh',
|
||||
$install_script_location => 'scripts/nightly/install.sh',
|
||||
];
|
||||
} else {
|
||||
$this->info('About to sync files PRODUCTION (docker-compose.yml, docker-compose.prod.yml, upgrade.sh, install.sh, etc) to BunnyCDN.');
|
||||
$fileMapping = [
|
||||
$compose_file_location => 'docker/docker-compose.yml',
|
||||
$compose_file_prod_location => 'docker/docker-compose.prod.yml',
|
||||
$production_env_location => 'environment/.env.production',
|
||||
$upgrade_script_location => 'scripts/upgrade.sh',
|
||||
$install_script_location => 'scripts/install.sh',
|
||||
];
|
||||
}
|
||||
|
||||
// BunnyCDN file mapping (local file => CDN URL path)
|
||||
$bunnyFileMapping = [
|
||||
$compose_file_location => "$bunny_cdn/$bunny_cdn_path/$compose_file",
|
||||
$compose_file_prod_location => "$bunny_cdn/$bunny_cdn_path/$compose_file_prod",
|
||||
$production_env_location => "$bunny_cdn/$bunny_cdn_path/$production_env",
|
||||
$upgrade_script_location => "$bunny_cdn/$bunny_cdn_path/$upgrade_script",
|
||||
$install_script_location => "$bunny_cdn/$bunny_cdn_path/$install_script",
|
||||
];
|
||||
|
||||
$diffTmpDir = sys_get_temp_dir().'/coolify-cdn-diff-'.time();
|
||||
@mkdir($diffTmpDir, 0755, true);
|
||||
$hasChanges = false;
|
||||
|
||||
// Diff against BunnyCDN
|
||||
$this->info('Fetching files from BunnyCDN to compare...');
|
||||
foreach ($bunnyFileMapping as $localFile => $cdnUrl) {
|
||||
if (! file_exists($localFile)) {
|
||||
$this->warn('Local file not found: '.$localFile);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$fileName = basename($cdnUrl);
|
||||
$remoteTmp = "$diffTmpDir/bunny-$fileName";
|
||||
|
||||
try {
|
||||
$response = Http::timeout(10)->get($cdnUrl);
|
||||
if ($response->successful()) {
|
||||
file_put_contents($remoteTmp, $response->body());
|
||||
$diffOutput = [];
|
||||
exec('diff -u '.escapeshellarg($remoteTmp).' '.escapeshellarg($localFile).' 2>&1', $diffOutput, $diffCode);
|
||||
if ($diffCode !== 0) {
|
||||
$hasChanges = true;
|
||||
$this->newLine();
|
||||
$this->info("--- BunnyCDN: $bunny_cdn_path/$fileName");
|
||||
$this->info("+++ Local: $fileName");
|
||||
foreach ($diffOutput as $line) {
|
||||
if (str_starts_with($line, '---') || str_starts_with($line, '+++')) {
|
||||
continue;
|
||||
}
|
||||
$this->line($line);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$this->info("NEW on BunnyCDN: $bunny_cdn_path/$fileName (HTTP {$response->status()})");
|
||||
$hasChanges = true;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->warn("Could not fetch $cdnUrl: {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
|
||||
// Diff against GitHub coolify-cdn repo
|
||||
$this->newLine();
|
||||
$this->info('Fetching coolify-cdn repo to compare...');
|
||||
$output = [];
|
||||
exec('gh repo clone coollabsio/coolify-cdn '.escapeshellarg("$diffTmpDir/repo").' -- --depth 1 2>&1', $output, $returnCode);
|
||||
|
||||
if ($returnCode === 0) {
|
||||
foreach ($fileMapping as $localFile => $cdnPath) {
|
||||
$remotePath = "$diffTmpDir/repo/$cdnPath";
|
||||
if (! file_exists($localFile)) {
|
||||
continue;
|
||||
}
|
||||
if (! file_exists($remotePath)) {
|
||||
$this->info("NEW on GitHub: $cdnPath (does not exist in coolify-cdn yet)");
|
||||
$hasChanges = true;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$diffOutput = [];
|
||||
exec('diff -u '.escapeshellarg($remotePath).' '.escapeshellarg($localFile).' 2>&1', $diffOutput, $diffCode);
|
||||
if ($diffCode !== 0) {
|
||||
$hasChanges = true;
|
||||
$this->newLine();
|
||||
$this->info("--- GitHub: $cdnPath");
|
||||
$this->info("+++ Local: $cdnPath");
|
||||
foreach ($diffOutput as $line) {
|
||||
if (str_starts_with($line, '---') || str_starts_with($line, '+++')) {
|
||||
continue;
|
||||
}
|
||||
$this->line($line);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$this->warn('Could not fetch coolify-cdn repo for diff.');
|
||||
}
|
||||
|
||||
exec('rm -rf '.escapeshellarg($diffTmpDir));
|
||||
|
||||
if (! $hasChanges) {
|
||||
$this->newLine();
|
||||
$this->info('No differences found. All files are already up to date.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
|
||||
$confirmed = confirm('Are you sure you want to sync?');
|
||||
if (! $confirmed) {
|
||||
return;
|
||||
@@ -692,7 +967,34 @@ class SyncBunny extends Command
|
||||
$pool->purge("$bunny_cdn/$bunny_cdn_path/$upgrade_script"),
|
||||
$pool->purge("$bunny_cdn/$bunny_cdn_path/$install_script"),
|
||||
]);
|
||||
$this->info('All files uploaded & purged...');
|
||||
$this->info('All files uploaded & purged to BunnyCDN.');
|
||||
$this->newLine();
|
||||
|
||||
// Sync files to GitHub CDN repository via PR
|
||||
$this->info('Creating GitHub PR for coolify-cdn repository...');
|
||||
if ($nightly) {
|
||||
$files = [
|
||||
$compose_file_location => 'docker/nightly/docker-compose.yml',
|
||||
$compose_file_prod_location => 'docker/nightly/docker-compose.prod.yml',
|
||||
$production_env_location => 'environment/nightly/.env.production',
|
||||
$upgrade_script_location => 'scripts/nightly/upgrade.sh',
|
||||
$install_script_location => 'scripts/nightly/install.sh',
|
||||
];
|
||||
} else {
|
||||
$files = [
|
||||
$compose_file_location => 'docker/docker-compose.yml',
|
||||
$compose_file_prod_location => 'docker/docker-compose.prod.yml',
|
||||
$production_env_location => 'environment/.env.production',
|
||||
$upgrade_script_location => 'scripts/upgrade.sh',
|
||||
$install_script_location => 'scripts/install.sh',
|
||||
];
|
||||
}
|
||||
|
||||
$githubSuccess = $this->syncFilesToGitHubRepo($files, $nightly);
|
||||
$this->newLine();
|
||||
$this->info('=== Summary ===');
|
||||
$this->info('BunnyCDN sync: Complete');
|
||||
$this->info('GitHub PR: '.($githubSuccess ? 'Created' : 'Failed'));
|
||||
} catch (\Throwable $e) {
|
||||
$this->error('Error: '.$e->getMessage());
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ class Kernel extends ConsoleKernel
|
||||
}
|
||||
|
||||
// $this->scheduleInstance->job(new CleanupStaleMultiplexedConnections)->hourly();
|
||||
$this->scheduleInstance->command('cleanup:redis')->weekly();
|
||||
$this->scheduleInstance->command('cleanup:redis --clear-locks')->daily();
|
||||
|
||||
if (isDev()) {
|
||||
// Instance Jobs
|
||||
|
||||
Reference in new issue
Block a user