From d3fbb32c527bf880244330c3ce655ad8a672e935 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 10 Jul 2026 14:29:11 +0200 Subject: [PATCH] feat(cdn): sync release metadata through BunnyCDN Replace the legacy sync:bunny flags with an interactive CDN sync flow for service templates and release metadata. Serve official service templates from the Coollabs CDN, update version metadata, and remove obsolete helper scripts. --- app/Console/Commands/SyncBunny.php | 321 +++++++++++++++--- .../Concerns/SummarizesDiffText.php | 2 +- config/constants.php | 4 +- other/nightly/versions.json | 4 +- scripts/conductor-setup.sh | 97 ------ scripts/sync_volume.sh | 57 ---- tests/Feature/PullChangelogTest.php | 2 +- tests/Feature/SyncBunnyTest.php | 232 ++++++++++--- .../ApplicationConfigurationSnapshotTest.php | 8 +- versions.json | 4 +- 10 files changed, 475 insertions(+), 256 deletions(-) delete mode 100755 scripts/conductor-setup.sh delete mode 100644 scripts/sync_volume.sh diff --git a/app/Console/Commands/SyncBunny.php b/app/Console/Commands/SyncBunny.php index 3f3e213fd..55acf3828 100644 --- a/app/Console/Commands/SyncBunny.php +++ b/app/Console/Commands/SyncBunny.php @@ -5,9 +5,12 @@ use Illuminate\Console\Command; use Illuminate\Http\Client\PendingRequest; use Illuminate\Http\Client\Pool; +use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Http; use function Laravel\Prompts\confirm; +use function Laravel\Prompts\multiselect; +use function Laravel\Prompts\select; class SyncBunny extends Command { @@ -16,7 +19,7 @@ class SyncBunny extends Command * * @var string */ - protected $signature = 'sync:bunny {--templates} {--release} {--nightly}'; + protected $signature = 'sync:bunny {--bunny}'; /** * The console command description. @@ -25,15 +28,234 @@ class SyncBunny extends Command */ protected $description = 'Sync files to BunnyCDN'; + protected function removeTemporaryDirectory(string $tmpDir): void + { + $temporaryRoot = realpath(sys_get_temp_dir()); + $temporaryDirectory = realpath($tmpDir); + + if ($temporaryRoot === false || $temporaryDirectory === false) { + return; + } + + $expectedPrefix = rtrim($temporaryRoot, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.'coollabs-cdn-'; + if (! str_starts_with($temporaryDirectory, $expectedPrefix)) { + return; + } + + File::deleteDirectory($temporaryDirectory); + } + + /** + * Fetch GitHub releases and sync to GitHub repository + */ + private function syncReleasesToGitHubRepo(array $files, bool $nightly = false): bool + { + $this->info('Fetching releases from GitHub...'); + try { + $response = Http::timeout(30) + ->get('https://api.github.com/repos/coollabsio/coolify/releases', [ + 'per_page' => 30, // Fetch more releases for better changelog + ]); + + if (! $response->successful()) { + $this->error('Failed to fetch releases from GitHub: '.$response->status()); + + return false; + } + + $releasesFile = tempnam(sys_get_temp_dir(), 'coolify-releases-'); + if ($releasesFile === false || file_put_contents($releasesFile, json_encode($response->json(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)) === false) { + $this->error('Failed to create temporary releases.json.'); + + return false; + } + + $files[$releasesFile] = $nightly ? 'json/coolify/nightly/releases.json' : 'json/coolify/releases.json'; + + try { + return $this->syncFilesToGitHubRepo($files, $nightly); + } finally { + @unlink($releasesFile); + } + } catch (\Throwable $e) { + $this->error('Error syncing releases: '.$e->getMessage()); + + return false; + } + } + + /** + * 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().'/coollabs-cdn-files-'.$timestamp; + $branchName = 'update-files-'.$timestamp; + + // Clone the repository + $this->info('Cloning coollabs-cdn repository...'); + $output = []; + exec('gh repo clone coollabsio/coollabs-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)); + $this->removeTemporaryDirectory($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"); + $this->removeTemporaryDirectory($tmpDir); + + return false; + } + } + + if (copy($sourceFile, $destPath) === false) { + $this->error("Failed to copy $sourceFile to $destPath"); + $this->removeTemporaryDirectory($tmpDir); + + return false; + } + + $copiedFiles[] = $targetPath; + $this->info("Copied: $targetPath"); + } + + if (empty($copiedFiles)) { + $this->warn('No files were copied. Nothing to commit.'); + $this->removeTemporaryDirectory($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)); + $this->removeTemporaryDirectory($tmpDir); + + return false; + } + + // Check for changes + $this->info('Checking for changes...'); + $changedFiles = []; + exec('cd '.escapeshellarg($tmpDir).' && git diff --cached --name-only 2>&1', $changedFiles, $returnCode); + if ($returnCode !== 0) { + $this->error('Failed to check changed files: '.implode("\n", $changedFiles)); + $this->removeTemporaryDirectory($tmpDir); + + return false; + } + + $changedFiles = array_values(array_filter($changedFiles)); + if (empty($changedFiles)) { + $this->info('All files are already up to date. No changes to commit.'); + $this->removeTemporaryDirectory($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)); + $this->removeTemporaryDirectory($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)); + $this->removeTemporaryDirectory($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- ", $changedFiles); + $prBody = "Automated update of $envLabel files:\n- $fileList"; + $prCommand = 'gh pr create --repo coollabsio/coollabs-cdn --title '.escapeshellarg($prTitle).' --body '.escapeshellarg($prBody).' --base main --head '.escapeshellarg($branchName).' 2>&1'; + $output = []; + exec($prCommand, $output, $returnCode); + + // Clean up + $this->removeTemporaryDirectory($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($changedFiles)); + + return true; + } catch (\Throwable $e) { + $this->error('Error syncing files to GitHub: '.$e->getMessage()); + + return false; + } + } + /** * Execute the console command. */ public function handle() { $that = $this; - $only_template = $this->option('templates'); - $only_version = $this->option('release'); - $nightly = $this->option('nightly'); + $only_bunny = $this->option('bunny'); + $nightly = select( + label: 'Which environment would you like to sync?', + options: [ + 'production' => 'Production', + 'nightly' => 'Nightly', + ], + default: 'production', + ) === 'nightly'; $bunny_cdn = 'https://cdn.coollabs.io'; $bunny_cdn_path = 'coolify'; $bunny_cdn_storage_name = 'coolcdn'; @@ -55,6 +277,7 @@ public function handle() $upgrade_script_location = "$parent_dir/scripts/upgrade.sh"; $upgrade_postgres_script_location = "$parent_dir/scripts/upgrade-postgres.sh"; $production_env_location = "$parent_dir/.env.production"; + $service_template_location = "$parent_dir/templates/$service_template"; $versions_location = "$parent_dir/$versions"; PendingRequest::macro('storage', function ($fileName) use ($that) { @@ -93,7 +316,7 @@ public function handle() $install_script_location = "$parent_dir/other/nightly/$install_script"; $versions_location = "$parent_dir/other/nightly/$versions"; } - if (! $only_template && ! $only_version) { + if ($only_bunny) { $envLabel = $nightly ? 'NIGHTLY' : 'PRODUCTION'; $this->info("About to sync $envLabel files to BunnyCDN."); $this->newLine(); @@ -108,7 +331,7 @@ public function handle() $install_script_location => "$bunny_cdn/$bunny_cdn_path/$install_script", ]; - $diffTmpDir = sys_get_temp_dir().'/coolify-cdn-diff-'.time(); + $diffTmpDir = sys_get_temp_dir().'/coollabs-cdn-diff-'.time(); @mkdir($diffTmpDir, 0755, true); $hasChanges = false; @@ -151,7 +374,7 @@ public function handle() } } - exec('rm -rf '.escapeshellarg($diffTmpDir)); + $this->removeTemporaryDirectory($diffTmpDir); if (! $hasChanges) { $this->newLine(); @@ -167,49 +390,55 @@ public function handle() return; } } - if ($only_template) { - $this->info('About to sync '.config('constants.services.file_name').' to BunnyCDN.'); - $confirmed = confirm('Are you sure you want to sync?'); - if (! $confirmed) { - return; - } - Http::pool(fn (Pool $pool) => [ - $pool->storage(fileName: "$parent_dir/templates/$service_template")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$service_template"), - $pool->purge("$bunny_cdn/$bunny_cdn_path/$service_template"), - ]); - $this->info('Service template uploaded & purged...'); + if (! $only_bunny) { + $envLabel = $nightly ? 'NIGHTLY' : 'PRODUCTION'; + $this->info("About to sync $envLabel releases, versions, compose, and environment files to GitHub repository."); - return; - } elseif ($only_version) { if ($nightly) { - $this->info('About to sync NIGHTLY versions.json to BunnyCDN.'); + $files = [ + $versions_location => 'json/coolify/nightly/versions.json', + $compose_file_location => 'json/coolify/nightly/docker-compose.yml', + $compose_file_prod_location => 'json/coolify/nightly/docker-compose.prod.yml', + $production_env_location => 'json/coolify/nightly/.env.production', + $install_script_location => 'json/coolify/nightly/install.sh', + $upgrade_script_location => 'json/coolify/nightly/upgrade.sh', + $upgrade_postgres_script_location => 'json/coolify/nightly/upgrade-postgres.sh', + $service_template_location => 'json/coolify/nightly/service-templates-latest.json', + ]; } else { - $this->info('About to sync PRODUCTION versions.json to BunnyCDN.'); - } - $file = file_get_contents($versions_location); - $json = json_decode($file, true); - $actual_version = data_get($json, 'coolify.v4.version'); - - $this->info("Version: {$actual_version}"); - $this->info('This will:'); - $this->info(' 1. Sync versions.json to BunnyCDN'); - $this->newLine(); - - $confirmed = confirm('Are you sure you want to proceed?'); - if (! $confirmed) { - return; + $files = [ + $versions_location => 'json/coolify/versions.json', + $compose_file_location => 'json/coolify/docker-compose.yml', + $compose_file_prod_location => 'json/coolify/docker-compose.prod.yml', + $production_env_location => 'json/coolify/.env.production', + $install_script_location => 'json/coolify/install.sh', + $upgrade_script_location => 'json/coolify/upgrade.sh', + $upgrade_postgres_script_location => 'json/coolify/upgrade-postgres.sh', + $service_template_location => 'json/coolify/service-templates-latest.json', + ]; } - $this->info('Syncing versions.json to BunnyCDN...'); - Http::pool(fn (Pool $pool) => [ - $pool->storage(fileName: $versions_location)->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$versions"), - $pool->purge("$bunny_cdn/$bunny_cdn_path/$versions"), - ]); - $this->info('✓ versions.json uploaded & purged to BunnyCDN'); - $this->newLine(); + $releasesTarget = $nightly ? 'json/coolify/nightly/releases.json' : 'json/coolify/releases.json'; + $options = [$releasesTarget, ...array_values($files)]; + $selectedFiles = multiselect( + label: 'Which files would you like to sync?', + options: $options, + default: $options, + required: true, + scroll: count($options), + ); - $this->info('=== Summary ==='); - $this->info('BunnyCDN sync: ✓ Complete'); + $includeReleases = in_array($releasesTarget, $selectedFiles, true); + $files = array_filter( + $files, + fn (string $targetPath) => in_array($targetPath, $selectedFiles, true), + ); + + if ($includeReleases) { + $this->syncReleasesToGitHubRepo($files, $nightly); + } else { + $this->syncFilesToGitHubRepo($files, $nightly); + } return; } @@ -231,10 +460,6 @@ public function handle() $pool->purge("$bunny_cdn/$bunny_cdn_path/$install_script"), ]); $this->info('All files uploaded & purged to BunnyCDN.'); - $this->newLine(); - - $this->info('=== Summary ==='); - $this->info('BunnyCDN sync: Complete'); } catch (\Throwable $e) { $this->error('Error: '.$e->getMessage()); } diff --git a/app/Services/DeploymentConfiguration/Concerns/SummarizesDiffText.php b/app/Services/DeploymentConfiguration/Concerns/SummarizesDiffText.php index 6960a8f1b..8eedf0920 100644 --- a/app/Services/DeploymentConfiguration/Concerns/SummarizesDiffText.php +++ b/app/Services/DeploymentConfiguration/Concerns/SummarizesDiffText.php @@ -9,7 +9,7 @@ trait SummarizesDiffText * worth expanding. Kept as one constant so the snapshot summary and the * differ's expand decision never drift apart. */ - private const SINGLE_LINE_LIMIT = 120; + private const SINGLE_LINE_LIMIT = 40; /** * Returns the value only when it is worth expanding (multi-line or longer diff --git a/config/constants.php b/config/constants.php index bf053fde3..290ce3f95 100644 --- a/config/constants.php +++ b/config/constants.php @@ -25,9 +25,7 @@ ], 'services' => [ - // Temporary disabled until cache is implemented - // 'official' => 'https://cdn.coollabs.io/coolify/service-templates.json', - 'official' => 'https://raw.githubusercontent.com/coollabsio/coolify/v4.x/templates/service-templates-latest.json', + 'official' => 'https://cdn.coollabs.io/coolify/service-templates-latest.json', 'file_name' => 'service-templates-latest.json', ], diff --git a/other/nightly/versions.json b/other/nightly/versions.json index 751db0754..9c9a405aa 100644 --- a/other/nightly/versions.json +++ b/other/nightly/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.2.0" + "version": "4.1.2" }, "nightly": { - "version": "4.2.1" + "version": "4.2.0" }, "helper": { "version": "1.0.14" diff --git a/scripts/conductor-setup.sh b/scripts/conductor-setup.sh deleted file mode 100755 index a88b457fb..000000000 --- a/scripts/conductor-setup.sh +++ /dev/null @@ -1,97 +0,0 @@ -#!/bin/bash -set -e - -# Validate CONDUCTOR_ROOT_PATH is set and valid before any operations -if [ -z "$CONDUCTOR_ROOT_PATH" ]; then - echo "ERROR: CONDUCTOR_ROOT_PATH environment variable is not set" - echo "This script must be run by Conductor with CONDUCTOR_ROOT_PATH set to the main repository path" - exit 1 -fi - -if [ ! -d "$CONDUCTOR_ROOT_PATH" ]; then - echo "ERROR: CONDUCTOR_ROOT_PATH ($CONDUCTOR_ROOT_PATH) is not a valid directory" - exit 1 -fi - -# Copy .env file -cp "$CONDUCTOR_ROOT_PATH/.env" .env - -# Setup shared dependencies via symlinks to main repo -echo "Setting up shared node_modules and vendor directories..." - -# Ensure main repo has the directories -mkdir -p "$CONDUCTOR_ROOT_PATH/node_modules" -mkdir -p "$CONDUCTOR_ROOT_PATH/vendor" - -# Get current worktree path -WORKTREE_PATH=$(pwd) - -# Safety check 1: ensure WORKTREE_PATH is valid -if [ -z "$WORKTREE_PATH" ]; then - echo "ERROR: WORKTREE_PATH is empty" - exit 1 -fi - -# Safety check 2: CRITICAL FIRST - blacklist system directories -# This check runs BEFORE the positive check to prevent dangerous operations -# even if someone misconfigures CONDUCTOR_ROOT_PATH -case "$WORKTREE_PATH" in - /|/bin|/sbin|/usr|/usr/*|/etc|/etc/*|/var|/var/*|/System|/System/*|/Library|/Library/*|/Applications|/Applications/*|"$HOME") - echo "ERROR: WORKTREE_PATH ($WORKTREE_PATH) is in a dangerous system location" - exit 1 - ;; -esac - -# Safety check 3: positive check - verify we're under CONDUCTOR_ROOT_PATH -case "$WORKTREE_PATH" in - "$CONDUCTOR_ROOT_PATH"|"$CONDUCTOR_ROOT_PATH"/.conductor/*) - # Valid: either main repo or under .conductor/ - ;; - *) - echo "ERROR: WORKTREE_PATH ($WORKTREE_PATH) is not under CONDUCTOR_ROOT_PATH ($CONDUCTOR_ROOT_PATH)" - exit 1 - ;; -esac - -# Safety check 4: verify we're in a git repository -if [ ! -f ".git" ] && [ ! -d ".git" ]; then - echo "ERROR: Not in a git repository" - exit 1 -fi - -# Remove existing directories/symlinks if they exist -# For symlinks: use 'rm' without -r to remove the symlink itself (not following it) -# For directories: use 'rm -rf' to remove the directory and contents -if [ -L "node_modules" ]; then - # It's a symlink - remove it without following (no -r flag) - rm "$WORKTREE_PATH/node_modules" -elif [ -e "node_modules" ]; then - # It's a regular directory or file - safe to use -rf - rm -rf "$WORKTREE_PATH/node_modules" -fi - -if [ -L "vendor" ]; then - # It's a symlink - remove it without following (no -r flag) - rm "$WORKTREE_PATH/vendor" -elif [ -e "vendor" ]; then - # It's a regular directory or file - safe to use -rf - rm -rf "$WORKTREE_PATH/vendor" -fi - -# Calculate relative path from worktree to main repo -# Use bash-native approach: try realpath first (GNU coreutils), fallback to perl -if command -v realpath &> /dev/null && realpath --relative-to / / &> /dev/null 2>&1; then - # GNU coreutils realpath with --relative-to support - RELATIVE_PATH=$(realpath --relative-to="$WORKTREE_PATH" "$CONDUCTOR_ROOT_PATH") -else - # Fallback: use perl which is standard on macOS and most Unix systems - RELATIVE_PATH=$(perl -e 'use File::Spec; print File::Spec->abs2rel($ARGV[0], $ARGV[1])' "$CONDUCTOR_ROOT_PATH" "$WORKTREE_PATH") -fi - -# Create symlinks to main repo's node_modules and vendor -ln -sf "$RELATIVE_PATH/node_modules" node_modules -ln -sf "$RELATIVE_PATH/vendor" vendor - -echo "✓ Shared dependencies linked successfully" -echo " node_modules -> $RELATIVE_PATH/node_modules" -echo " vendor -> $RELATIVE_PATH/vendor" \ No newline at end of file diff --git a/scripts/sync_volume.sh b/scripts/sync_volume.sh deleted file mode 100644 index 43631fdf7..000000000 --- a/scripts/sync_volume.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/bash -# Sync docker volumes between two servers - -VERSION="1.0.0" -SOURCE=$1 -DESTINATION=$2 -set -e -if [ -z "$SOURCE" ]; then - echo "Source server is not specified." - exit 1 -fi -if [ -z "$DESTINATION" ]; then - echo "Destination server is not specified." - exit 1 -fi - -SOURCE_USER=$(echo $SOURCE | cut -d@ -f1) -SOURCE_SERVER=$(echo $SOURCE | cut -d: -f1 | cut -d@ -f2) -SOURCE_PORT=$(echo $SOURCE | cut -d: -f2 | cut -d/ -f1) -SOURCE_VOLUME_NAME=$(echo $SOURCE | cut -d/ -f2) - -if ! [[ "$SOURCE_PORT" =~ ^[0-9]+$ ]]; then - echo "Invalid source port: $SOURCE_PORT" - exit 1 -fi - -DESTINATION_USER=$(echo $DESTINATION | cut -d@ -f1) -DESTINATION_SERVER=$(echo $DESTINATION | cut -d: -f1 | cut -d@ -f2) -DESTINATION_PORT=$(echo $DESTINATION | cut -d: -f2 | cut -d/ -f1) -DESTINATION_VOLUME_NAME=$(echo $DESTINATION | cut -d/ -f2) - -if ! [[ "$DESTINATION_PORT" =~ ^[0-9]+$ ]]; then - echo "Invalid destination port: $DESTINATION_PORT" - exit 1 -fi - -echo "Generating backup file to ./$SOURCE_VOLUME_NAME.tgz" -ssh -p $SOURCE_PORT $SOURCE_USER@$SOURCE_SERVER "docker run -v $SOURCE_VOLUME_NAME:/volume --rm --log-driver none loomchild/volume-backup backup -c pigz -v" >./$SOURCE_VOLUME_NAME.tgz -echo "" -if [ -f "./$SOURCE_VOLUME_NAME.tgz" ]; then - echo "Uploading backup file to $DESTINATION_SERVER:~/$DESTINATION_VOLUME_NAME.tgz" - scp -P $DESTINATION_PORT ./$SOURCE_VOLUME_NAME.tgz $DESTINATION_USER@$DESTINATION_SERVER:~/$DESTINATION_VOLUME_NAME.tgz - echo "" - echo "Restoring backup file on remote ($DESTINATION_SERVER:/~/$DESTINATION_VOLUME_NAME.tgz)" - ssh -p $DESTINATION_PORT $DESTINATION_USER@$DESTINATION_SERVER "docker run -i -v $DESTINATION_VOLUME_NAME:/volume --log-driver none --rm loomchild/volume-backup restore -c pigz -vf < ~/$DESTINATION_VOLUME_NAME.tgz" - echo "" - echo "Deleting backup file on remote ($DESTINATION_SERVER:/~/$DESTINATION_VOLUME_NAME.tgz)" - ssh -p $DESTINATION_PORT $DESTINATION_USER@$DESTINATION_SERVER "rm ~/$DESTINATION_VOLUME_NAME.tgz" - - echo "" - echo "Local file ./$SOURCE_VOLUME_NAME.tgz is not deleted." - - echo "" - echo "WARNING: If you are copying a database volume, you need to set the right users/passwords on the destination service's environment variables." - echo "Why? Because we are copying the volume as-is, so the database credentials will bethe same as on the source volume." -fi - diff --git a/tests/Feature/PullChangelogTest.php b/tests/Feature/PullChangelogTest.php index 145638812..7793b0b77 100644 --- a/tests/Feature/PullChangelogTest.php +++ b/tests/Feature/PullChangelogTest.php @@ -34,7 +34,7 @@ function fakeReleasesPayload(): array test('releases_url config defaults to the GitHub raw source', function () { expect(config('constants.coolify.releases_url')) - ->toBe('https://raw.githubusercontent.com/coollabsio/coolify-cdn/main/json/releases.json'); + ->toBe('https://cdn.coollabs.io/coolify/service-templates-latest.json'); }); test('PullChangelog fetches from the configured releases_url and writes the changelog', function () { diff --git a/tests/Feature/SyncBunnyTest.php b/tests/Feature/SyncBunnyTest.php index ca3091841..9f4badad3 100644 --- a/tests/Feature/SyncBunnyTest.php +++ b/tests/Feature/SyncBunnyTest.php @@ -1,63 +1,107 @@ > "$SYNC_BUNNY_TEST_LOG" -exit 1 -SH); + file_put_contents("{$binDir}/{$name}", $contents); chmod("{$binDir}/{$name}", 0755); } -it('syncs nightly versions to BunnyCDN without creating a GitHub PR', function () { - Http::fake([ - 'storage.bunnycdn.com/*' => Http::response([], 201), - 'api.bunny.net/purge*' => Http::response([], 200), - ]); +it('only exposes the BunnyCDN legacy sync option', function () { + $definition = Artisan::all()['sync:bunny']->getDefinition(); - $binDir = sys_get_temp_dir().'/sync-bunny-bin-'.uniqid(); - $logFile = sys_get_temp_dir().'/sync-bunny-'.uniqid().'.log'; - - mkdir($binDir, 0755, true); - createSyncBunnyFailingBinary($binDir, 'gh'); - createSyncBunnyFailingBinary($binDir, 'git'); - - $originalPath = getenv('PATH') ?: ''; - putenv("PATH={$binDir}:{$originalPath}"); - putenv("SYNC_BUNNY_TEST_LOG={$logFile}"); - - try { - $this->artisan('sync:bunny --release --nightly') - ->expectsConfirmation('Are you sure you want to proceed?', 'yes') - ->expectsOutputToContain('BunnyCDN sync: ✓ Complete') - ->doesntExpectOutputToContain('GitHub PR') - ->assertExitCode(0); - } finally { - putenv("PATH={$originalPath}"); - putenv('SYNC_BUNNY_TEST_LOG'); - } - - expect(file_exists($logFile))->toBeFalse(); - - Http::assertSent(fn ($request) => $request->url() === 'https://storage.bunnycdn.com/coolcdn/coolify-nightly/versions.json'); - Http::assertSent(fn ($request) => str_starts_with($request->url(), 'https://api.bunny.net/purge') - && $request['url'] === 'https://cdn.coollabs.io/coolify-nightly/versions.json'); + expect($definition->hasOption('bunny'))->toBeTrue() + ->and($definition->hasOption('github-releases'))->toBeFalse() + ->and($definition->hasOption('release'))->toBeFalse() + ->and($definition->hasOption('nightly'))->toBeFalse() + ->and($definition->hasOption('templates'))->toBeFalse(); }); -it('syncs postgres upgrade script to BunnyCDN during full sync', function () { +it('loads service templates from the Coollabs CDN', function () { + expect(config('constants.services.official')) + ->toBe('https://cdn.coollabs.io/coolify/service-templates-latest.json'); +}); + +it('only removes validated Coolify CDN temporary directories', function () { + $command = new class extends SyncBunny + { + public function removeDirectory(string $path): void + { + $this->removeTemporaryDirectory($path); + } + }; + + $invalidDirectory = sys_get_temp_dir().'/unrelated-directory-'.uniqid(); + $validDirectory = sys_get_temp_dir().'/coollabs-cdn-files-'.uniqid(); + mkdir($invalidDirectory); + mkdir($validDirectory); + + $command->removeDirectory(''); + $command->removeDirectory($invalidDirectory); + $command->removeDirectory($validDirectory); + + expect($invalidDirectory)->toBeDirectory() + ->and($validDirectory)->not->toBeDirectory(); + + rmdir($invalidDirectory); +}); + +it('syncs full files to BunnyCDN only when explicitly requested', function () { Http::fake([ 'https://cdn.coollabs.io/coolify/*' => Http::response('', 404), 'https://storage.bunnycdn.com/*' => Http::response([], 201), 'https://api.bunny.net/purge*' => Http::response([], 200), ]); - $this->artisan('sync:bunny') - ->expectsConfirmation('Are you sure you want to sync?', 'yes') - ->expectsOutputToContain('BunnyCDN sync: Complete') - ->assertExitCode(0); + $binDir = sys_get_temp_dir().'/sync-bunny-bin-'.uniqid(); + $logFile = sys_get_temp_dir().'/sync-bunny-'.uniqid().'.log'; + + mkdir($binDir, 0755, true); + + createFakeSyncBunnyBinary($binDir, 'gh', <<<'SH' +#!/bin/sh +printf 'gh %s\n' "$*" >> "$SYNC_BUNNY_TEST_LOG" +if [ "$1" = "repo" ] && [ "$2" = "clone" ]; then + mkdir -p "$4/scripts" +fi +exit 0 +SH); + + createFakeSyncBunnyBinary($binDir, 'git', <<<'SH' +#!/bin/sh +printf 'git %s\n' "$*" >> "$SYNC_BUNNY_TEST_LOG" +if [ "$1" = "status" ]; then + printf 'M scripts/upgrade-postgres.sh\n' +fi +exit 0 +SH); + + $originalPath = getenv('PATH') ?: ''; + putenv("PATH={$binDir}:{$originalPath}"); + putenv("SYNC_BUNNY_TEST_LOG={$logFile}"); + + try { + $this->artisan('sync:bunny --bunny') + ->expectsChoice('Which environment would you like to sync?', 'production', [ + 'production' => 'Production', + 'nightly' => 'Nightly', + ]) + ->expectsConfirmation('Are you sure you want to sync?', 'yes') + ->assertExitCode(0); + } finally { + putenv("PATH={$originalPath}"); + putenv('SYNC_BUNNY_TEST_LOG'); + } + + $log = file_exists($logFile) ? file_get_contents($logFile) : ''; + + expect($log) + ->not->toContain('gh repo clone') + ->not->toContain('gh pr create') + ->not->toContain('coollabsio/coolify-cdn'); Http::assertSent(fn ($request) => $request->method() === 'PUT' && $request->url() === 'https://storage.bunnycdn.com/coolcdn/coolify/upgrade-postgres.sh'); @@ -65,3 +109,105 @@ function createSyncBunnyFailingBinary(string $binDir, string $name): void Http::assertSent(fn ($request) => str_starts_with($request->url(), 'https://api.bunny.net/purge') && $request['url'] === 'https://cdn.coollabs.io/coolify/upgrade-postgres.sh'); }); + +it('selects the environment and release files to sync to GitHub', function (string $targetDirectory, string $environment, array $selectedBasenames) { + Http::fake([ + 'api.github.com/repos/coollabsio/coolify/releases*' => Http::response([], 200), + ]); + + $binDir = sys_get_temp_dir().'/sync-bunny-bin-'.uniqid(); + $logFile = sys_get_temp_dir().'/sync-bunny-'.uniqid().'.log'; + + mkdir($binDir, 0755, true); + + createFakeSyncBunnyBinary($binDir, 'gh', <<<'SH' +#!/bin/sh +printf 'gh %s\n' "$*" >> "$SYNC_BUNNY_TEST_LOG" +if [ "$1" = "repo" ] && [ "$2" = "clone" ]; then + mkdir -p "$4" +fi +exit 0 +SH); + + createFakeSyncBunnyBinary($binDir, 'git', <<<'SH' +#!/bin/sh +printf 'git %s\n' "$*" >> "$SYNC_BUNNY_TEST_LOG" +if [ "$1" = "status" ]; then + printf 'M json/releases.json\n' +fi +if [ "$1" = "diff" ]; then + if [ -f json/coolify/nightly/releases.json ]; then + printf 'json/coolify/nightly/releases.json\n' + else + printf 'json/coolify/releases.json\n' + fi +fi +exit 0 +SH); + + $originalPath = getenv('PATH') ?: ''; + putenv("PATH={$binDir}:{$originalPath}"); + putenv("SYNC_BUNNY_TEST_LOG={$logFile}"); + + $allBasenames = [ + 'releases.json', + 'versions.json', + 'docker-compose.yml', + 'docker-compose.prod.yml', + '.env.production', + 'install.sh', + 'upgrade.sh', + 'upgrade-postgres.sh', + 'service-templates-latest.json', + ]; + $allTargets = array_map(fn (string $file) => "$targetDirectory/$file", $allBasenames); + $selectedTargets = array_map(fn (string $file) => "$targetDirectory/$file", $selectedBasenames); + + try { + $this->artisan('sync:bunny') + ->expectsChoice('Which environment would you like to sync?', $environment, [ + 'production' => 'Production', + 'nightly' => 'Nightly', + ]) + ->expectsChoice('Which files would you like to sync?', $selectedTargets, $allTargets) + ->assertExitCode(0); + } finally { + putenv("PATH={$originalPath}"); + putenv('SYNC_BUNNY_TEST_LOG'); + } + + $log = file_get_contents($logFile); + + expect($log) + ->toContain('gh pr create --repo coollabsio/coollabs-cdn') + ->not->toContain('coollabsio/coolify-cdn'); + + foreach ($selectedTargets as $selectedTarget) { + expect($log)->toContain($selectedTarget); + } + + foreach (array_diff($allTargets, $selectedTargets) as $unselectedTarget) { + expect($log)->not->toContain($unselectedTarget); + } + + $pullRequestCommand = substr($log, strrpos($log, 'gh pr create')); + + expect($pullRequestCommand) + ->toContain("$targetDirectory/releases.json") + ->not->toContain("$targetDirectory/versions.json"); + + Http::assertSentCount(1); +})->with([ + 'select production files' => ['json/coolify', 'production', ['releases.json', 'versions.json']], + 'select nightly with all files selected by default' => ['json/coolify/nightly', 'nightly', [ + 'releases.json', + 'versions.json', + 'docker-compose.yml', + 'docker-compose.prod.yml', + '.env.production', + 'install.sh', + 'upgrade.sh', + 'upgrade-postgres.sh', + 'service-templates-latest.json', + ]], +]); diff --git a/tests/Unit/DeploymentConfiguration/ApplicationConfigurationSnapshotTest.php b/tests/Unit/DeploymentConfiguration/ApplicationConfigurationSnapshotTest.php index 20b7c0adc..c6c823633 100644 --- a/tests/Unit/DeploymentConfiguration/ApplicationConfigurationSnapshotTest.php +++ b/tests/Unit/DeploymentConfiguration/ApplicationConfigurationSnapshotTest.php @@ -66,12 +66,16 @@ function markSnapshotTestApplicationDeployed(Application $application): Applicat $application = snapshotTestApplication(); markSnapshotTestApplicationDeployed($application); - $application->update(['fqdn' => 'https://new.example.com']); + $domains = 'https://new.example.com,https://another.example.com'; + $application->update(['fqdn' => $domains]); $diff = $application->refresh()->pendingDeploymentConfigurationDiff(); + $change = collect($diff->changes())->firstWhere('label', 'Domains'); expect($diff->isChanged())->toBeTrue() ->and($diff->requiresBuild())->toBeFalse() - ->and(collect($diff->changes())->pluck('label'))->toContain('Domains'); + ->and($change)->not->toBeNull() + ->and($change['expandable'])->toBeTrue() + ->and($change['new_full_value'])->toBe($domains); }); it('detects environment variable value changes without exposing secret values', function () { diff --git a/versions.json b/versions.json index 751db0754..9c9a405aa 100644 --- a/versions.json +++ b/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.2.0" + "version": "4.1.2" }, "nightly": { - "version": "4.2.1" + "version": "4.2.0" }, "helper": { "version": "1.0.14"