mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-04 10:42:23 +00:00
feat(release): add sync-cdn.sh to sync a release to the CDN
- Replaces php artisan sync:cdn and its sync:cdn task in scripts/run - --purge clears the Cloudflare cache sooner than the 10 minutes it keeps a file, with the token and zone from the environment or .env
This commit is contained in:
7 files changed
+144
-623
No files matched your search
+1
-1
@@ -17,7 +17,7 @@ indent_size = 2
|
||||
[docker-compose.yml]
|
||||
indent_size = 4
|
||||
|
||||
[scripts/clean-manifests.sh]
|
||||
[scripts/{clean-manifests,sync-cdn}.sh]
|
||||
indent_style = tab
|
||||
binary_next_line = true
|
||||
switch_case_indent = true
|
||||
|
||||
@@ -58,7 +58,6 @@ VITE_PORT=5173
|
||||
# Selenium Driver URL for Dusk
|
||||
DUSK_DRIVER_URL=http://selenium:4444
|
||||
|
||||
# Special Keys for Andras
|
||||
# For Cloudflare cache purging (token needs Zone > Cache Purge)
|
||||
# For scripts/sync-cdn.sh --purge, a Cloudflare token that can purge the cache of the zone (Zone > Cache Purge)
|
||||
CLOUDFLARE_API_TOKEN=
|
||||
CLOUDFLARE_ZONE_ID=
|
||||
@@ -1,399 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
use function Laravel\Prompts\multiselect;
|
||||
use function Laravel\Prompts\select;
|
||||
|
||||
class SyncCdn extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'sync:cdn {--purge : Purge the Cloudflare cache for the CDN files}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Sync CDN files to the coollabs-cdn GitHub repository';
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge the given CDN URLs from the Cloudflare cache.
|
||||
*
|
||||
* @param array<int, string> $urls
|
||||
*/
|
||||
private function purgeCloudflareCache(array $urls): bool
|
||||
{
|
||||
$apiToken = config('constants.cloudflare.api_token');
|
||||
$zoneId = config('constants.cloudflare.zone_id');
|
||||
|
||||
if (blank($apiToken) || blank($zoneId)) {
|
||||
$this->error('CLOUDFLARE_API_TOKEN and CLOUDFLARE_ZONE_ID must be set to purge the Cloudflare cache.');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($urls as $url) {
|
||||
$this->info('Purging: '.$url);
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::withToken($apiToken)
|
||||
->timeout(30)
|
||||
->post("https://api.cloudflare.com/client/v4/zones/{$zoneId}/purge_cache", [
|
||||
'files' => array_values($urls),
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
$this->error('Failed to purge Cloudflare cache: '.$e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! $response->successful() || $response->json('success') !== true) {
|
||||
$this->error('Failed to purge Cloudflare cache: '.$response->body());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->info('Cloudflare cache purged.');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()
|
||||
{
|
||||
$nightly = select(
|
||||
label: 'Which environment would you like to sync?',
|
||||
options: [
|
||||
'production' => 'Production',
|
||||
'nightly' => 'Nightly',
|
||||
],
|
||||
default: 'production',
|
||||
) === 'nightly';
|
||||
$cdn = 'https://cdn.coollabs.io';
|
||||
|
||||
$parent_dir = realpath(dirname(__FILE__).'/../../..');
|
||||
|
||||
$compose_file = 'docker-compose.yml';
|
||||
$compose_file_prod = 'docker-compose.prod.yml';
|
||||
$install_script = 'install.sh';
|
||||
$upgrade_script = 'upgrade.sh';
|
||||
$upgrade_postgres_script = 'upgrade-postgres.sh';
|
||||
$production_env = '.env.production';
|
||||
$service_template = config('constants.services.file_name');
|
||||
$versions = 'versions.json';
|
||||
|
||||
$compose_file_location = "$parent_dir/$compose_file";
|
||||
$compose_file_prod_location = "$parent_dir/$compose_file_prod";
|
||||
$install_script_location = "$parent_dir/scripts/install.sh";
|
||||
$upgrade_script_location = "$parent_dir/scripts/upgrade.sh";
|
||||
$upgrade_postgres_script_location = "$parent_dir/scripts/upgrade-postgres.sh";
|
||||
$patch_script_location = "$parent_dir/scripts/patch.sh";
|
||||
$production_env_location = "$parent_dir/.env.production";
|
||||
$service_template_location = "$parent_dir/templates/$service_template";
|
||||
$versions_location = "$parent_dir/$versions";
|
||||
|
||||
try {
|
||||
if ($nightly) {
|
||||
$compose_file_location = "$parent_dir/other/nightly/$compose_file";
|
||||
$compose_file_prod_location = "$parent_dir/other/nightly/$compose_file_prod";
|
||||
$production_env_location = "$parent_dir/other/nightly/$production_env";
|
||||
$upgrade_script_location = "$parent_dir/other/nightly/$upgrade_script";
|
||||
$upgrade_postgres_script_location = "$parent_dir/other/nightly/$upgrade_postgres_script";
|
||||
$install_script_location = "$parent_dir/other/nightly/$install_script";
|
||||
$versions_location = "$parent_dir/other/nightly/$versions";
|
||||
}
|
||||
if ($nightly) {
|
||||
$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 {
|
||||
$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',
|
||||
$patch_script_location => 'json/coolify/patch.sh',
|
||||
$service_template_location => 'json/coolify/service-templates-latest.json',
|
||||
];
|
||||
}
|
||||
|
||||
$releasesTarget = $nightly ? 'json/coolify/nightly/releases.json' : 'json/coolify/releases.json';
|
||||
|
||||
if ($this->option('purge')) {
|
||||
$urls = array_map(
|
||||
fn (string $targetPath) => $cdn.'/'.substr($targetPath, strlen('json/')),
|
||||
[$releasesTarget, ...array_values($files)],
|
||||
);
|
||||
if ($nightly) {
|
||||
// Nightly scripts still download from the legacy /coolify-nightly/ path, which Cloudflare caches separately.
|
||||
$urls = [...$urls, ...str_replace('/coolify/nightly/', '/coolify-nightly/', $urls)];
|
||||
}
|
||||
|
||||
return $this->purgeCloudflareCache($urls) ? self::SUCCESS : self::FAILURE;
|
||||
}
|
||||
|
||||
$envLabel = $nightly ? 'NIGHTLY' : 'PRODUCTION';
|
||||
$this->info("About to sync $envLabel releases, versions, compose, and environment files to GitHub repository.");
|
||||
|
||||
$options = [$releasesTarget, ...array_values($files)];
|
||||
$selectedFiles = multiselect(
|
||||
label: 'Which files would you like to sync?',
|
||||
options: $options,
|
||||
default: $options,
|
||||
required: true,
|
||||
scroll: count($options),
|
||||
);
|
||||
|
||||
$includeReleases = in_array($releasesTarget, $selectedFiles, true);
|
||||
$files = array_filter(
|
||||
$files,
|
||||
fn (string $targetPath) => in_array($targetPath, $selectedFiles, true),
|
||||
);
|
||||
|
||||
$synced = $includeReleases
|
||||
? $this->syncReleasesToGitHubRepo($files, $nightly)
|
||||
: $this->syncFilesToGitHubRepo($files, $nightly);
|
||||
|
||||
if ($synced) {
|
||||
$this->info('After the CDN pull request is merged and deployed, run "php artisan sync:cdn --purge" to purge the Cloudflare cache.');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->error('Error: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -139,11 +139,6 @@ return [
|
||||
'dev_webhook' => env('SERVEO_URL'),
|
||||
],
|
||||
|
||||
'cloudflare' => [
|
||||
'api_token' => env('CLOUDFLARE_API_TOKEN'),
|
||||
'zone_id' => env('CLOUDFLARE_ZONE_ID'),
|
||||
],
|
||||
|
||||
'server_checks' => [
|
||||
// Notification delay configuration for parallel server checks
|
||||
// Used for Traefik version checks and other future server check jobs
|
||||
|
||||
@@ -27,10 +27,6 @@ function test {
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml exec -T coolify php artisan test --testsuite=Feature -p
|
||||
}
|
||||
|
||||
function sync:cdn {
|
||||
php artisan sync:cdn --env=secrets
|
||||
}
|
||||
|
||||
function db:reset {
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml exec -u www-data coolify php artisan migrate:fresh --seed
|
||||
}
|
||||
|
||||
Executable
+142
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
OPTION="$*"
|
||||
CHANNEL="${1:-}"
|
||||
VERSION="${2:-}"
|
||||
CDN_REPOSITORY=coollabsio/coollabs-cdn
|
||||
CDN_URL=https://cdn.coollabs.io
|
||||
FILES=versions.json,releases.json,docker-compose.yml,docker-compose.prod.yml,.env.production,install.sh,upgrade.sh,upgrade-postgres.sh,patch.sh,service-templates-latest.json
|
||||
WORK_DIR="$(mktemp -d)"
|
||||
|
||||
BOLD=$'\033[1m' BLUE=$'\033[34m' GREEN=$'\033[32m' RED=$'\033[31m' RESET=$'\033[0m'
|
||||
|
||||
info() { printf ' %sℹ%s %s\n' "$BLUE" "$RESET" "$*"; }
|
||||
ok() { printf ' %s✔%s %s\n' "$GREEN" "$RESET" "$*"; }
|
||||
die() {
|
||||
printf ' %s✖%s %s\n' "$RED" "$RESET" "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
trap 'rm -rf "$WORK_DIR"' EXIT
|
||||
|
||||
main() {
|
||||
[[ "$OPTION" =~ ^(--stable [0-9]+\.[0-9]+\.[0-9]+|--preview [0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta|rc)\.[0-9]+)?|--purge)$ ]] \
|
||||
|| die "'$OPTION' is not an option, expected --stable or --preview with the version of the channel, or --purge"
|
||||
if [ "$OPTION" = --purge ]; then
|
||||
purge_cache
|
||||
return
|
||||
fi
|
||||
select_channel
|
||||
write_versions
|
||||
fetch_releases
|
||||
clone_cdn
|
||||
write_release
|
||||
commit_channel
|
||||
publish_channel
|
||||
}
|
||||
|
||||
select_channel() {
|
||||
channel="${CHANNEL#--}"
|
||||
channel_path=json/coolify channel_url="$CDN_URL/coolify"
|
||||
if [ "$channel" != stable ]; then
|
||||
channel_path+="/$channel" channel_url+="/$channel"
|
||||
fi
|
||||
if [[ "$channel:$VERSION" == preview:*-alpha.* ]]; then
|
||||
info "the preview channel takes no alphas, Coolify $VERSION is not synced"
|
||||
exit 0
|
||||
fi
|
||||
info "syncing $channel_url to Coolify $BOLD$VERSION$RESET"
|
||||
}
|
||||
|
||||
write_versions() {
|
||||
jq --indent 4 --arg version "$VERSION" '.coolify.v4.version = $version' versions.json > "$WORK_DIR/versions.json"
|
||||
ok "versions.json names Coolify $VERSION and the helper, Sentinel and Traefik versions of this commit"
|
||||
}
|
||||
|
||||
fetch_releases() {
|
||||
gh api "repos/coollabsio/coolify/releases?per_page=30" | jq . > "$WORK_DIR/releases.json"
|
||||
ok "releases.json lists the $(jq length "$WORK_DIR/releases.json") newest GitHub releases"
|
||||
}
|
||||
|
||||
clone_cdn() {
|
||||
gh repo clone "$CDN_REPOSITORY" "$WORK_DIR/cdn" -- --quiet --depth 1
|
||||
ok "cloned $CDN_REPOSITORY"
|
||||
}
|
||||
|
||||
write_release() {
|
||||
local release="$WORK_DIR/cdn/json/coolify/release.json"
|
||||
[ -f "$release" ] || echo '{}' > "$release"
|
||||
jq --tab --arg channel "$channel" --arg version "$VERSION" '.[$channel].coolify = $version | {stable, preview} | with_entries(select(.value))' \
|
||||
"$release" > "$WORK_DIR/release.json"
|
||||
cp "$WORK_DIR/release.json" "$release"
|
||||
ok "release.json sets the $channel version $BOLD$VERSION$RESET"
|
||||
}
|
||||
|
||||
commit_channel() {
|
||||
local name count
|
||||
mkdir -p "$WORK_DIR/cdn/$channel_path"
|
||||
for name in ${FILES//,/ }; do
|
||||
channel_file "$name" > "$WORK_DIR/cdn/$channel_path/$name"
|
||||
done
|
||||
cdn add "$channel_path" json/coolify/release.json
|
||||
if cdn diff --cached --quiet; then
|
||||
ok "$channel_path on $CDN_REPOSITORY is up to date"
|
||||
exit 0
|
||||
fi
|
||||
title="chore: sync the $channel channel to Coolify $VERSION"
|
||||
count="$(cdn diff --cached --name-only | wc -l | tr -d ' ')"
|
||||
cdn commit --quiet --message "$title"
|
||||
ok "committed $count changed file(s) of $channel_path"
|
||||
}
|
||||
|
||||
publish_channel() {
|
||||
local previous from branch pull_request
|
||||
if [ "$channel" != stable ]; then
|
||||
cdn push --quiet origin HEAD:main
|
||||
ok "pushed the $channel channel to main of $CDN_REPOSITORY"
|
||||
return
|
||||
fi
|
||||
previous="$(cdn show HEAD~1:json/coolify/versions.json 2> /dev/null | jq -r '.coolify.v4.version // empty' || true)"
|
||||
[ "$previous" = "$VERSION" ] || from="${previous:+from Coolify $previous to }"
|
||||
branch="sync/stable-$(date -u +%Y%m%d%H%M%S)"
|
||||
cdn push --quiet origin "HEAD:refs/heads/$branch"
|
||||
pull_request="$(gh pr create --repo "$CDN_REPOSITORY" --base main --head "$branch" --title "$title" \
|
||||
--body "Syncs the stable channel ${from:-to Coolify }$VERSION, see the [release notes](https://github.com/coollabsio/coolify/releases/tag/v$VERSION).")"
|
||||
ok "opened $pull_request"
|
||||
info "Cloudflare keeps the old files for up to 10 minutes, scripts/sync-cdn.sh --purge clears them once this pull request is merged and deployed"
|
||||
}
|
||||
|
||||
purge_cache() {
|
||||
local token zone
|
||||
token="${CLOUDFLARE_API_TOKEN:-$(env_value CLOUDFLARE_API_TOKEN)}"
|
||||
zone="${CLOUDFLARE_ZONE_ID:-$(env_value CLOUDFLARE_ZONE_ID)}"
|
||||
[ -n "$token" ] && [ -n "$zone" ] || die "CLOUDFLARE_API_TOKEN and CLOUDFLARE_ZONE_ID are set neither in the environment nor in .env, expected a token that can purge the cache of the zone"
|
||||
clone_cdn
|
||||
cdn ls-files json/coolify | sed "s|^json/|$CDN_URL/|" | xargs -n 30 | while read -r urls; do
|
||||
curl --proto '=https' --tlsv1.2 -sSf --retry 3 -H "Authorization: Bearer $token" -H "Content-Type: application/json" \
|
||||
--data "$(tr ' ' '\n' <<< "$urls" | jq -Rnc '{files: [inputs]}')" "https://api.cloudflare.com/client/v4/zones/$zone/purge_cache" \
|
||||
| jq -e .success > /dev/null || die "Cloudflare did not purge the files of $CDN_URL/coolify"
|
||||
done
|
||||
ok "purged every file of $CDN_URL/coolify from the Cloudflare cache"
|
||||
}
|
||||
|
||||
channel_file() {
|
||||
case "$1" in
|
||||
versions.json | releases.json) cat "$WORK_DIR/$1" ;;
|
||||
service-templates-latest.json) cat "templates/$1" ;;
|
||||
*.sh) sed "s|^CDN=\"$CDN_URL/coolify\"$|CDN=\"$channel_url\"|" "scripts/$1" ;;
|
||||
*) cat "$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
cdn() {
|
||||
git -C "$WORK_DIR/cdn" "$@"
|
||||
}
|
||||
|
||||
env_value() {
|
||||
[ ! -f .env ] || sed -n "s/^$1=//p" .env | tail -n 1 | tr -d "\"'"
|
||||
}
|
||||
|
||||
main
|
||||
@@ -1,212 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Console\Commands\SyncCdn;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
function createFakeSyncCdnBinary(string $binDir, string $name, string $contents): void
|
||||
{
|
||||
file_put_contents("{$binDir}/{$name}", $contents);
|
||||
chmod("{$binDir}/{$name}", 0755);
|
||||
}
|
||||
|
||||
it('does not expose the removed sync options', function () {
|
||||
$definition = Artisan::all()['sync:cdn']->getDefinition();
|
||||
|
||||
expect($definition->hasOption('bunny'))->toBeFalse()
|
||||
->and($definition->hasOption('github-releases'))->toBeFalse()
|
||||
->and($definition->hasOption('release'))->toBeFalse()
|
||||
->and($definition->hasOption('nightly'))->toBeFalse()
|
||||
->and($definition->hasOption('templates'))->toBeFalse();
|
||||
});
|
||||
|
||||
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 SyncCdn
|
||||
{
|
||||
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('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-cdn-bin-'.uniqid();
|
||||
$logFile = sys_get_temp_dir().'/sync-cdn-'.uniqid().'.log';
|
||||
|
||||
mkdir($binDir, 0755, true);
|
||||
|
||||
createFakeSyncCdnBinary($binDir, 'gh', <<<'SH'
|
||||
#!/bin/sh
|
||||
printf 'gh %s\n' "$*" >> "$SYNC_CDN_TEST_LOG"
|
||||
if [ "$1" = "repo" ] && [ "$2" = "clone" ]; then
|
||||
mkdir -p "$4"
|
||||
fi
|
||||
exit 0
|
||||
SH);
|
||||
|
||||
createFakeSyncCdnBinary($binDir, 'git', <<<'SH'
|
||||
#!/bin/sh
|
||||
printf 'git %s\n' "$*" >> "$SYNC_CDN_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_CDN_TEST_LOG={$logFile}");
|
||||
|
||||
$allBasenames = [
|
||||
'releases.json',
|
||||
'versions.json',
|
||||
'docker-compose.yml',
|
||||
'docker-compose.prod.yml',
|
||||
'.env.production',
|
||||
'install.sh',
|
||||
'upgrade.sh',
|
||||
'upgrade-postgres.sh',
|
||||
...($environment === 'production' ? ['patch.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:cdn')
|
||||
->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_CDN_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',
|
||||
]],
|
||||
]);
|
||||
|
||||
it('purges the CDN files from the Cloudflare cache', function (string $environment, array $cdnDirectories) {
|
||||
config()->set('constants.cloudflare.api_token', 'test-token');
|
||||
config()->set('constants.cloudflare.zone_id', 'test-zone');
|
||||
|
||||
Http::fake([
|
||||
'https://api.cloudflare.com/*' => Http::response(['success' => true], 200),
|
||||
]);
|
||||
|
||||
$this->artisan('sync:cdn --purge')
|
||||
->expectsChoice('Which environment would you like to sync?', $environment, [
|
||||
'production' => 'Production',
|
||||
'nightly' => 'Nightly',
|
||||
])
|
||||
->assertExitCode(0);
|
||||
|
||||
Http::assertSentCount(1);
|
||||
Http::assertSent(fn ($request) => $request->method() === 'POST'
|
||||
&& $request->url() === 'https://api.cloudflare.com/client/v4/zones/test-zone/purge_cache'
|
||||
&& $request->hasHeader('Authorization', 'Bearer test-token')
|
||||
&& $request['files'] === collect($cdnDirectories)->flatMap(fn (string $cdnDirectory) => array_map(fn (string $file) => "https://cdn.coollabs.io/$cdnDirectory/$file", [
|
||||
'releases.json',
|
||||
'versions.json',
|
||||
'docker-compose.yml',
|
||||
'docker-compose.prod.yml',
|
||||
'.env.production',
|
||||
'install.sh',
|
||||
'upgrade.sh',
|
||||
'upgrade-postgres.sh',
|
||||
...($environment === 'production' ? ['patch.sh'] : []),
|
||||
'service-templates-latest.json',
|
||||
]))->all());
|
||||
})->with([
|
||||
'production' => ['production', ['coolify']],
|
||||
'nightly with legacy path' => ['nightly', ['coolify/nightly', 'coolify-nightly']],
|
||||
]);
|
||||
|
||||
it('fails the Cloudflare purge without credentials or on an API error', function (?string $apiToken, array $response) {
|
||||
config()->set('constants.cloudflare.api_token', $apiToken);
|
||||
config()->set('constants.cloudflare.zone_id', 'test-zone');
|
||||
|
||||
Http::fake([
|
||||
'https://api.cloudflare.com/*' => Http::response($response, 400),
|
||||
]);
|
||||
|
||||
$this->artisan('sync:cdn --purge')
|
||||
->expectsChoice('Which environment would you like to sync?', 'production', [
|
||||
'production' => 'Production',
|
||||
'nightly' => 'Nightly',
|
||||
])
|
||||
->assertExitCode(1);
|
||||
|
||||
if ($apiToken === null) {
|
||||
Http::assertNothingSent();
|
||||
}
|
||||
})->with([
|
||||
'missing token' => [null, []],
|
||||
'api error' => ['test-token', ['success' => false, 'errors' => [['code' => 10000, 'message' => 'Authentication error']]]],
|
||||
]);
|
||||
Reference in new issue
Block a user