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.
This commit is contained in:
parent
e4f925ebbf
commit
d3fbb32c52
10 changed files with 475 additions and 256 deletions
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
],
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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 () {
|
||||
|
|
|
|||
|
|
@ -1,63 +1,107 @@
|
|||
<?php
|
||||
|
||||
use App\Console\Commands\SyncBunny;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
function createSyncBunnyFailingBinary(string $binDir, string $name): void
|
||||
function createFakeSyncBunnyBinary(string $binDir, string $name, string $contents): void
|
||||
{
|
||||
file_put_contents("{$binDir}/{$name}", <<<'SH'
|
||||
#!/bin/sh
|
||||
printf '%s %s\n' "$(basename "$0")" "$*" >> "$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',
|
||||
]],
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -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 () {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue