From 7560d0745d7990472439ac589418157626a4d11a Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:25:24 +0100 Subject: [PATCH] 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 --- .editorconfig | 2 +- .env.development.example | 3 +- app/Console/Commands/SyncCdn.php | 399 ------------------------------- config/constants.php | 5 - scripts/run | 4 - scripts/sync-cdn.sh | 142 +++++++++++ tests/Feature/SyncCdnTest.php | 212 ---------------- 7 files changed, 144 insertions(+), 623 deletions(-) delete mode 100644 app/Console/Commands/SyncCdn.php create mode 100755 scripts/sync-cdn.sh delete mode 100644 tests/Feature/SyncCdnTest.php diff --git a/.editorconfig b/.editorconfig index c998050b7f..1da225e99b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -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 diff --git a/.env.development.example b/.env.development.example index 7c8fc5c028..2f474260a9 100644 --- a/.env.development.example +++ b/.env.development.example @@ -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= diff --git a/app/Console/Commands/SyncCdn.php b/app/Console/Commands/SyncCdn.php deleted file mode 100644 index c447c5f726..0000000000 --- a/app/Console/Commands/SyncCdn.php +++ /dev/null @@ -1,399 +0,0 @@ - $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()); - } - } -} diff --git a/config/constants.php b/config/constants.php index 2bacce35e1..f4728c24c8 100644 --- a/config/constants.php +++ b/config/constants.php @@ -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 diff --git a/scripts/run b/scripts/run index d605cfc574..9dbeada869 100755 --- a/scripts/run +++ b/scripts/run @@ -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 } diff --git a/scripts/sync-cdn.sh b/scripts/sync-cdn.sh new file mode 100755 index 0000000000..23635ba5a0 --- /dev/null +++ b/scripts/sync-cdn.sh @@ -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 diff --git a/tests/Feature/SyncCdnTest.php b/tests/Feature/SyncCdnTest.php deleted file mode 100644 index 52e0ea8945..0000000000 --- a/tests/Feature/SyncCdnTest.php +++ /dev/null @@ -1,212 +0,0 @@ -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']]]], -]);