mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-04 10:42:23 +00:00
feat(release): streamline versions.json on the CDN
- Every channel has a block of its own, every version after this one reads only these blocks - Every stable sync also fills the coolify and traefik keys older instances read - An instance keeps a copy of versions.json and reads upstream.json of the image until it has one
This commit is contained in:
1 parent
d759126149
commit
0e30950793
24 files changed
+196
-309
No files matched your search
@@ -8,7 +8,7 @@
|
||||
|
||||
Open source & free forever, backed by our [philosophy](https://coolify.io/philosophy).
|
||||
|
||||

|
||||
|
||||
[Website](https://coolify.io) · [Documentation](https://coolify.io/docs) · [Cloud](https://app.coolify.io) · [Discord](https://coollabs.io/discord) · [Community](https://github.com/coollabsio/coolify/discussions)
|
||||
|
||||
+3
-4
@@ -160,7 +160,7 @@ The pull request workflow builds every pull request and publishes the image of a
|
||||
|
||||
## CDN
|
||||
|
||||
`scripts/sync-cdn.sh` syncs a channel of `coollabsio/coollabs-cdn` from the commit of the version it syncs: the install and upgrade scripts, the compose files, the environment file, the service templates, the GitHub releases and `versions.json`.
|
||||
`scripts/sync-cdn.sh` syncs a channel of `coollabsio/coollabs-cdn` from the commit of the version it syncs: the install and upgrade scripts, the compose files, the environment file, the service templates, the GitHub releases and the block of the channel in `/coolify/versions.json`.
|
||||
|
||||
| Channel | Path | Synced by |
|
||||
| --------- | ------------------- | --------------------------------------------------------- |
|
||||
@@ -170,9 +170,8 @@ The pull request workflow builds every pull request and publishes the image of a
|
||||
|
||||
- The preview channel takes betas and release candidates, alphas reach the nightly channel only
|
||||
- `CDN_TOKEN` in the environment `release` is a fine-grained token for `coollabsio/coollabs-cdn` alone, with read and write access to contents and pull requests
|
||||
- `versions.json` of a channel names its Coolify version and the helper, Sentinel and Traefik versions of `upstream.json`
|
||||
- `/coolify/release.json` lists the Coolify version of every channel, each sync sets the version of its channel
|
||||
- An instance on the preview or nightly channel takes the newest version of its channel and the more stable ones from `/coolify/release.json`
|
||||
- `/coolify/versions.json` has a `stable`, `preview` and `nightly` block, each with the Coolify version of its channel and the helper, Sentinel and Traefik versions of the `upstream.json` it was synced from. An instance takes the newest version of its channel and the more stable ones
|
||||
- Every stable sync also sets the `coolify` and `traefik` keys, which older instances read, to the stable versions, so older instances upgrade to a version that reads the blocks
|
||||
|
||||
Cloudflare keeps a file of the CDN for up to 10 minutes. To clear the cache sooner, for example once the pull request of the stable channel is merged and deployed, set `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ZONE_ID` in `.env` or the environment, with a token that can purge the cache of the zone:
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ class UpdateCoolify
|
||||
$latest = fetch_latest_versions(tries: $manual_update ? 1 : 3);
|
||||
|
||||
if ($latest !== null) {
|
||||
$this->latestVersion = data_get($latest, 'versions.coolify.v4.version');
|
||||
$this->latestVersion = data_get($latest, 'versions.coolify.version');
|
||||
$this->upgradeScriptUrl = $latest['upgrade_script_url'];
|
||||
} else {
|
||||
// Fallback to cache if CDN unavailable
|
||||
@@ -71,6 +71,7 @@ class UpdateCoolify
|
||||
}
|
||||
|
||||
$this->latestVersion = $cacheVersion;
|
||||
$this->upgradeScriptUrl = data_get(get_versions_data(), 'upgrade_script_url');
|
||||
Log::warning('Failed to fetch fresh version from CDN (unsuccessful response), using validated cache', [
|
||||
'version' => $cacheVersion,
|
||||
]);
|
||||
@@ -92,6 +93,7 @@ class UpdateCoolify
|
||||
}
|
||||
|
||||
$this->latestVersion = $cacheVersion;
|
||||
$this->upgradeScriptUrl = data_get(get_versions_data(), 'upgrade_script_url');
|
||||
Log::warning('Failed to fetch fresh version from CDN, using validated cache', [
|
||||
'error' => $e->getMessage(),
|
||||
'version' => $cacheVersion,
|
||||
|
||||
@@ -132,7 +132,7 @@ class SentinelController extends Controller
|
||||
|
||||
Cache::put(Server::sentinelReportedVersionCacheKey($server->id), $runningVersion, now()->addHours(2));
|
||||
|
||||
$latestVersion = data_get(get_versions_data(), 'coolify.sentinel.version');
|
||||
$latestVersion = data_get(get_versions_data(), 'sentinel.version');
|
||||
if (blank($latestVersion) || version_compare($runningVersion, $latestVersion, '>=')) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue
|
||||
/** A manual check runs in the web request, so it does not wait for CDN retries. */
|
||||
public int $cdnTries = 3;
|
||||
|
||||
public function __construct(int $cdnTries = 3, public bool $channelChanged = false)
|
||||
public function __construct(int $cdnTries = 3)
|
||||
{
|
||||
$this->cdnTries = $cdnTries;
|
||||
}
|
||||
@@ -31,14 +31,14 @@ class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue
|
||||
if (isDev() || isCloud()) {
|
||||
return false;
|
||||
}
|
||||
$versions = refresh_versions_file($this->cdnTries, $this->channelChanged);
|
||||
$versions = refresh_versions_file($this->cdnTries);
|
||||
if ($versions === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
CheckTraefikVersionJob::dispatch();
|
||||
|
||||
$latestVersion = data_get($versions, 'coolify.v4.version');
|
||||
$latestVersion = data_get($versions, 'coolify.version');
|
||||
$newVersionAvailable = instanceSettings()->update_channel !== 'custom'
|
||||
&& version_compare($latestVersion, config('constants.coolify.version'), '>');
|
||||
instanceSettings()->update(['new_version_available' => $newVersionAvailable]);
|
||||
|
||||
@@ -83,7 +83,8 @@ class Updates extends Component
|
||||
$changedFields = auditChangedFields($this->settings);
|
||||
$this->settings->save();
|
||||
if ($channelChanged) {
|
||||
(new CheckForUpdatesJob(cdnTries: 1, channelChanged: true))->handle();
|
||||
invalidate_versions_cache();
|
||||
(new CheckForUpdatesJob(cdnTries: 1))->handle();
|
||||
$this->dispatch('updateAvailable');
|
||||
}
|
||||
if ($changedFields !== []) {
|
||||
|
||||
@@ -715,14 +715,14 @@ function get_route_parameters(): array
|
||||
function get_latest_sentinel_version(): string
|
||||
{
|
||||
// Read the local versions.json, which CheckForUpdatesJob refreshes every hour. Do not ask the CDN for each server.
|
||||
return data_get(get_versions_data(), 'coolify.sentinel.version') ?? '0.0.0';
|
||||
return data_get(get_versions_data(), 'sentinel.version') ?? '0.0.0';
|
||||
}
|
||||
function get_latest_version_of_coolify(): string
|
||||
{
|
||||
try {
|
||||
$versions = get_versions_data();
|
||||
|
||||
return data_get($versions, 'coolify.v4.version', '0.0.0');
|
||||
return data_get($versions, 'coolify.version', '0.0.0');
|
||||
} catch (Throwable $e) {
|
||||
|
||||
return '0.0.0';
|
||||
|
||||
@@ -9,24 +9,30 @@ use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Get cached versions data from versions.json.
|
||||
* Get the versions of the channel this instance follows: its Coolify version and the helper, Sentinel and Traefik versions released with it.
|
||||
*
|
||||
* This function provides a centralized, cached access point for all
|
||||
* version data in the application. Data is cached in Redis for 1 hour
|
||||
* and shared across all servers in the cluster.
|
||||
* version data in the application. It reads the copy of versions.json of the CDN
|
||||
* that refresh_versions_file() keeps on this node, or the upstream.json of the image until then.
|
||||
* Data is cached in Redis for 1 hour and shared across all servers in the cluster.
|
||||
*
|
||||
* @return array|null The versions data array, or null if file doesn't exist
|
||||
* @return array{coolify: array{version: string}, helper?: array{version: string}, sentinel?: array{version: string}, traefik?: array<string, string>}
|
||||
*/
|
||||
function get_versions_data(): ?array
|
||||
function get_versions_data(): array
|
||||
{
|
||||
return Cache::remember('coolify:versions:all', 3600, function () {
|
||||
$versionsPath = File::exists(storage_path('app/versions.json')) ? storage_path('app/versions.json') : base_path('versions.json');
|
||||
|
||||
return Cache::remember('coolify:versions', 3600, function () {
|
||||
$versionsPath = storage_path('app/versions.json');
|
||||
if (! File::exists($versionsPath)) {
|
||||
return null;
|
||||
return ['coolify' => ['version' => config('constants.coolify.version')]] + json_decode(File::get(base_path('upstream.json')), true);
|
||||
}
|
||||
$channels = json_decode(File::get($versionsPath), true);
|
||||
$channel = newest_update_channel($channels);
|
||||
$versions = $channels[$channel] + ['upgrade_script_url' => upgrade_script_url($channel)];
|
||||
if (version_compare(config('constants.coolify.version'), $versions['coolify']['version'], '>')) {
|
||||
$versions['coolify']['version'] = config('constants.coolify.version');
|
||||
}
|
||||
|
||||
return json_decode(File::get($versionsPath), true);
|
||||
return $versions;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -54,7 +60,7 @@ function get_traefik_versions(): ?array
|
||||
*/
|
||||
function invalidate_versions_cache(): void
|
||||
{
|
||||
Cache::forget('coolify:versions:all');
|
||||
Cache::forget('coolify:versions');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,41 +95,41 @@ function cdn_retry_delay_ms(int $attempt, Throwable $exception): int
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{versions: array, upgrade_script_url: string}|null
|
||||
* Fetch versions.json from the CDN and take the block of the newest Coolify version this instance takes.
|
||||
* Only the stable, preview and nightly keys are read, the coolify and traefik keys are for older instances.
|
||||
*
|
||||
* @return array{channels: array, versions: array{coolify: array{version: string}}, upgrade_script_url: string}|null
|
||||
*/
|
||||
function fetch_latest_versions(int $tries = 3): ?array
|
||||
{
|
||||
$settings = instanceSettings();
|
||||
$cdnUrl = dirname(config('constants.coolify.versions_url'));
|
||||
$channel = 'stable';
|
||||
if ($settings->update_channel !== 'stable' || $settings->previous_update_channel !== null) {
|
||||
$response = cdn_http($tries)->get("{$cdnUrl}/release.json");
|
||||
$release = $response->json();
|
||||
if ($response->failed() || ! is_array($release)) {
|
||||
return null;
|
||||
}
|
||||
$channel = newest_update_channel(array_map(fn ($channel) => data_get($channel, 'coolify'), $release));
|
||||
}
|
||||
|
||||
$channelUrl = $channel === 'stable' ? null : "{$cdnUrl}/{$channel}";
|
||||
$response = cdn_http($tries)->get($channelUrl ? "{$channelUrl}/versions.json" : config('constants.coolify.versions_url'));
|
||||
$versions = $response->json();
|
||||
if ($response->failed() || ! is_string(data_get($versions, 'coolify.v4.version'))) {
|
||||
$response = cdn_http($tries)->get(config('constants.coolify.versions_url'));
|
||||
$channels = $response->json();
|
||||
if ($response->failed() || ! is_array($channels)) {
|
||||
return null;
|
||||
}
|
||||
$channel = newest_update_channel($channels);
|
||||
|
||||
return [
|
||||
'versions' => $versions,
|
||||
'upgrade_script_url' => $channelUrl ? "{$channelUrl}/upgrade.sh" : config('constants.coolify.upgrade_script_url'),
|
||||
'channels' => $channels,
|
||||
'versions' => $channels[$channel],
|
||||
'upgrade_script_url' => upgrade_script_url($channel),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* The upgrade script of an update channel on the CDN.
|
||||
*/
|
||||
function upgrade_script_url(string $channel): string
|
||||
{
|
||||
return $channel === 'stable' ? config('constants.coolify.upgrade_script_url') : dirname(config('constants.coolify.versions_url'))."/{$channel}/upgrade.sh";
|
||||
}
|
||||
|
||||
/**
|
||||
* The channel of the newest Coolify version this instance takes: its update channel and the more stable ones.
|
||||
* After a switch to a more stable channel, also the channel it left, below the stable release of the running version,
|
||||
* so an instance on a nightly of 4.6.0 gets the nightlies of 4.6.0 until 4.6.0 is released.
|
||||
*
|
||||
* @param array<string, mixed> $versions The Coolify version of each channel
|
||||
* @param array<string, mixed> $versions The versions.json of the CDN
|
||||
*/
|
||||
function newest_update_channel(array $versions): string
|
||||
{
|
||||
@@ -134,8 +140,8 @@ function newest_update_channel(array $versions): string
|
||||
$release = preg_replace('/^(\d+\.\d+\.\d+).*/', '$1', config('constants.coolify.version'));
|
||||
$newest = 'stable';
|
||||
foreach ($channels as $rank => $channel) {
|
||||
$version = $versions[$channel] ?? null;
|
||||
if (! is_string($version) || version_compare($version, (string) ($versions[$newest] ?? ''), '<=')) {
|
||||
$version = data_get($versions, "{$channel}.coolify.version");
|
||||
if (! is_string($version) || version_compare($version, (string) data_get($versions, "{$newest}.coolify.version"), '<=')) {
|
||||
continue;
|
||||
}
|
||||
if ($rank <= $follows || ($rank <= $followsUntilRelease && version_compare($version, $release, '<'))) {
|
||||
@@ -147,62 +153,50 @@ function newest_update_channel(array $versions): string
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch versions.json from the CDN and replace the local file of this node with it in one step.
|
||||
* The Coolify version never goes below the running version, or below the cached one while the channel stays.
|
||||
* When the CDN fails, the local file stays as it is. After a channel change it is removed instead,
|
||||
* so the versions of the image apply until the next check and not the version of the old channel.
|
||||
* Fetch versions.json from the CDN and replace the copy of it on this node in one step.
|
||||
* The copy stays when the CDN serves an older Coolify version for this instance than the copy has, or when the CDN fails.
|
||||
*
|
||||
* @return array|null The written versions data, or null if the CDN fetch failed
|
||||
* @return array|null The versions of the channel of this instance, or null if the CDN fetch failed
|
||||
*/
|
||||
function refresh_versions_file(int $tries = 3, bool $channelChanged = false): ?array
|
||||
function refresh_versions_file(int $tries = 3): ?array
|
||||
{
|
||||
$versionsPath = storage_path('app/versions.json');
|
||||
$versions = fetch_latest_versions($tries)['versions'] ?? null;
|
||||
if ($versions === null) {
|
||||
$latest = fetch_latest_versions($tries);
|
||||
if ($latest === null) {
|
||||
Log::warning('Could not fetch versions.json from the CDN, using the local file');
|
||||
if ($channelChanged) {
|
||||
File::delete($versionsPath);
|
||||
invalidate_versions_cache();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$latestVersion = data_get($versions, 'coolify.v4.version');
|
||||
$latestVersion = data_get($latest, 'versions.coolify.version');
|
||||
$currentVersion = config('constants.coolify.version');
|
||||
|
||||
$existingCoolifyVersion = null;
|
||||
if (! $channelChanged && File::exists($versionsPath)) {
|
||||
$existingVersions = json_decode(File::get($versionsPath), true);
|
||||
$existingCoolifyVersion = data_get($existingVersions, 'coolify.v4.version');
|
||||
if (File::exists($versionsPath)) {
|
||||
$existingChannels = json_decode(File::get($versionsPath), true);
|
||||
$existingCoolifyVersion = data_get($existingChannels, newest_update_channel($existingChannels).'.coolify.version');
|
||||
}
|
||||
|
||||
$bestVersion = $latestVersion;
|
||||
|
||||
if ($existingCoolifyVersion && version_compare($existingCoolifyVersion, $bestVersion, '>')) {
|
||||
if ($existingCoolifyVersion && version_compare($existingCoolifyVersion, $latestVersion, '>')) {
|
||||
Log::warning('CDN served older Coolify version than cache', [
|
||||
'cdn_version' => $latestVersion,
|
||||
'cached_version' => $existingCoolifyVersion,
|
||||
'current_version' => $currentVersion,
|
||||
]);
|
||||
$bestVersion = $existingCoolifyVersion;
|
||||
|
||||
return get_versions_data();
|
||||
}
|
||||
|
||||
if (version_compare($bestVersion, $currentVersion, '<')) {
|
||||
if (version_compare($latestVersion, $currentVersion, '<')) {
|
||||
Log::warning('Version downgrade prevented in CheckForUpdatesJob', [
|
||||
'cdn_version' => $latestVersion,
|
||||
'cached_version' => $existingCoolifyVersion,
|
||||
'current_version' => $currentVersion,
|
||||
'attempted_best' => $bestVersion,
|
||||
'using' => $currentVersion,
|
||||
]);
|
||||
$bestVersion = $currentVersion;
|
||||
}
|
||||
|
||||
data_set($versions, 'coolify.v4.version', $bestVersion);
|
||||
|
||||
File::replace($versionsPath, json_encode($versions, JSON_PRETTY_PRINT));
|
||||
File::replace($versionsPath, json_encode($latest['channels'], JSON_PRETTY_PRINT));
|
||||
invalidate_versions_cache();
|
||||
$versions = get_versions_data();
|
||||
update_helper_version($versions);
|
||||
|
||||
return $versions;
|
||||
@@ -213,7 +207,7 @@ function refresh_versions_file(int $tries = 3, bool $channelChanged = false): ?a
|
||||
*/
|
||||
function update_helper_version(?array $versions): void
|
||||
{
|
||||
$latestVersion = data_get($versions, 'coolify.helper.version');
|
||||
$latestVersion = data_get($versions, 'helper.version');
|
||||
$settings = instanceSettings();
|
||||
if ($latestVersion && version_compare($latestVersion, (string) $settings->helper_version, '>')) {
|
||||
$settings->update(['helper_version' => $latestVersion]);
|
||||
|
||||
@@ -204,8 +204,6 @@ COPY --chown=www-data:www-data artisan artisan
|
||||
COPY --chown=www-data:www-data openapi.yaml ./openapi.yaml
|
||||
COPY --chown=www-data:www-data changelogs/ ./changelogs/
|
||||
COPY --chown=www-data:www-data release.json upstream.json ./
|
||||
RUN jq --arg version "$COOLIFY_VERSION" '{coolify: ({v4: {$version}} + del(.traefik)), traefik}' upstream.json > versions.json \
|
||||
&& chown www-data:www-data versions.json
|
||||
|
||||
RUN composer dump-autoload
|
||||
|
||||
|
||||
+5
-3
@@ -16,6 +16,7 @@ set -e # Exit immediately if a command exits with a non-zero status
|
||||
#set -u # Treat unset variables as an error and exit
|
||||
set -o pipefail # Cause a pipeline to return the status of the last command that exited with a non-zero status
|
||||
CDN="https://cdn.coollabs.io/coolify"
|
||||
CHANNEL="stable"
|
||||
DATE=$(date +"%Y%m%d-%H%M%S")
|
||||
|
||||
OS_TYPE=$(grep -w "ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
|
||||
@@ -518,9 +519,10 @@ if [ "$OS_TYPE" = 'amzn' ]; then
|
||||
fi
|
||||
|
||||
# Fetch versions.json once and parse all values from it
|
||||
VERSIONS_JSON=$(curl -L --silent $CDN/versions.json)
|
||||
LATEST_VERSION=$(echo "$VERSIONS_JSON" | sed -n '/"v4"/,/}/s/.*"version": *"\([^"]*\)".*/\1/p' | head -n 1)
|
||||
LATEST_HELPER_VERSION=$(echo "$VERSIONS_JSON" | sed -n '/"helper"/,/}/s/.*"version": *"\([^"]*\)".*/\1/p' | head -n 1)
|
||||
VERSIONS_JSON=$(curl -L --silent https://cdn.coollabs.io/coolify/versions.json)
|
||||
CHANNEL_VERSIONS=$(echo "$VERSIONS_JSON" | sed -n "/^ \"$CHANNEL\": {/,/^ }/p")
|
||||
LATEST_VERSION=$(echo "$CHANNEL_VERSIONS" | sed -n '/"coolify"/,/}/s/.*"version": *"\([^"]*\)".*/\1/p' | head -n 1)
|
||||
LATEST_HELPER_VERSION=$(echo "$CHANNEL_VERSIONS" | sed -n '/"helper"/,/}/s/.*"version": *"\([^"]*\)".*/\1/p' | head -n 1)
|
||||
|
||||
if [ -z "$LATEST_HELPER_VERSION" ]; then
|
||||
LATEST_HELPER_VERSION=latest
|
||||
|
||||
+20
-19
@@ -6,7 +6,7 @@ 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
|
||||
FILES=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'
|
||||
@@ -29,10 +29,9 @@ main() {
|
||||
return
|
||||
fi
|
||||
select_channel
|
||||
write_versions
|
||||
fetch_releases
|
||||
clone_cdn
|
||||
write_release
|
||||
write_versions
|
||||
commit_channel
|
||||
publish_channel
|
||||
}
|
||||
@@ -50,11 +49,6 @@ select_channel() {
|
||||
info "syncing $channel_url to Coolify $BOLD$VERSION$RESET"
|
||||
}
|
||||
|
||||
write_versions() {
|
||||
jq --indent 4 --arg version "$VERSION" '{coolify: ({v4: {$version}} + del(.traefik)), traefik}' upstream.json > "$WORK_DIR/versions.json"
|
||||
ok "versions.json names Coolify $VERSION and the helper, Sentinel and Traefik versions of upstream.json"
|
||||
}
|
||||
|
||||
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"
|
||||
@@ -65,13 +59,17 @@ clone_cdn() {
|
||||
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, nightly} | 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"
|
||||
write_versions() {
|
||||
local versions="$WORK_DIR/cdn/json/coolify/versions.json"
|
||||
[ -f "$versions" ] || echo '{}' > "$versions"
|
||||
jq --indent 4 --arg channel "$channel" --arg version "$VERSION" --slurpfile upstream upstream.json '
|
||||
.[$channel] = ({coolify: {$version}} + $upstream[0])
|
||||
| if $channel == "stable" then .coolify = ({v4: {$version}} + ($upstream[0] | del(.traefik))) | .traefik = $upstream[0].traefik else . end
|
||||
| {stable, preview, nightly, coolify, traefik} | with_entries(select(.value))
|
||||
' "$versions" > "$WORK_DIR/versions.json"
|
||||
cp "$WORK_DIR/versions.json" "$versions"
|
||||
ok "versions.json sets the $channel version $BOLD$VERSION$RESET with the helper, Sentinel and Traefik versions of upstream.json"
|
||||
[ "$channel" != stable ] || ok "versions.json sets the coolify and traefik keys of older instances to the same versions"
|
||||
}
|
||||
|
||||
commit_channel() {
|
||||
@@ -80,7 +78,10 @@ commit_channel() {
|
||||
for name in ${FILES//,/ }; do
|
||||
channel_file "$name" > "$WORK_DIR/cdn/$channel_path/$name"
|
||||
done
|
||||
cdn add "$channel_path" json/coolify/release.json
|
||||
if [ "$channel" != stable ]; then
|
||||
cdn rm --quiet --ignore-unmatch "$channel_path/versions.json"
|
||||
fi
|
||||
cdn add "$channel_path" json/coolify/versions.json
|
||||
if cdn diff --cached --quiet; then
|
||||
ok "$channel_path on $CDN_REPOSITORY is up to date"
|
||||
exit 0
|
||||
@@ -98,7 +99,7 @@ publish_channel() {
|
||||
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="$(cdn show HEAD~1:json/coolify/versions.json 2> /dev/null | jq -r '.stable.coolify.version // .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"
|
||||
@@ -124,9 +125,9 @@ purge_cache() {
|
||||
|
||||
channel_file() {
|
||||
case "$1" in
|
||||
versions.json | releases.json) cat "$WORK_DIR/$1" ;;
|
||||
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" ;;
|
||||
*.sh) sed -e "s|^CDN=\"$CDN_URL/coolify\"$|CDN=\"$channel_url\"|" -e "s|^CHANNEL=\"stable\"$|CHANNEL=\"$channel\"|" "scripts/$1" ;;
|
||||
*) cat "$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
@@ -5,22 +5,21 @@ use App\Jobs\CheckTraefikVersionJob;
|
||||
use App\Models\InstanceSettings;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
afterEach(function () {
|
||||
@unlink(storage_path('app/versions.json'));
|
||||
});
|
||||
|
||||
it('checks installed Traefik versions after refreshing available versions', function () {
|
||||
Bus::fake();
|
||||
Http::fake([
|
||||
'*' => Http::response([
|
||||
'coolify' => ['v4' => ['version' => '4.0.10']],
|
||||
'traefik' => ['v3.7' => '3.7.13'],
|
||||
'stable' => ['coolify' => ['version' => '4.0.10'], 'traefik' => ['v3.7' => '3.7.13']],
|
||||
]),
|
||||
]);
|
||||
File::shouldReceive('exists')->andReturn(false);
|
||||
File::shouldReceive('replace')->once();
|
||||
|
||||
InstanceSettings::forceCreate(['id' => 0]);
|
||||
|
||||
config([
|
||||
@@ -32,4 +31,5 @@ it('checks installed Traefik versions after refreshing available versions', func
|
||||
(new CheckForUpdatesJob)->handle();
|
||||
|
||||
Bus::assertDispatched(CheckTraefikVersionJob::class);
|
||||
expect(get_traefik_versions())->toBe(['v3.7' => '3.7.13']);
|
||||
});
|
||||
@@ -12,6 +12,10 @@ use Illuminate\Support\Sleep;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
afterEach(function () {
|
||||
@unlink(storage_path('app/versions.json'));
|
||||
});
|
||||
|
||||
beforeEach(function () {
|
||||
InstanceSettings::forceCreate(['id' => 0]);
|
||||
|
||||
@@ -24,7 +28,7 @@ beforeEach(function () {
|
||||
|
||||
it('uses a newer helper version from the local versions file without asking the CDN', function () {
|
||||
Http::preventStrayRequests();
|
||||
Cache::put('coolify:versions:all', ['coolify' => ['helper' => ['version' => '1.0.18']]], 3600);
|
||||
Cache::put('coolify:versions', ['coolify' => ['version' => '4.0.0'], 'helper' => ['version' => '1.0.18']], 3600);
|
||||
|
||||
(new CheckHelperImageJob)->handle();
|
||||
|
||||
@@ -33,18 +37,17 @@ it('uses a newer helper version from the local versions file without asking the
|
||||
});
|
||||
|
||||
it('stores the helper version when the update check refreshes versions.json from the CDN', function () {
|
||||
File::partialMock()->shouldReceive('replace')->once()->with(storage_path('app/versions.json'), Mockery::type('string'));
|
||||
config(['constants.coolify.version' => '4.0.0']);
|
||||
Http::preventStrayRequests();
|
||||
Http::fake([
|
||||
'https://cdn.example.com/coolify/versions.json' => Http::response([
|
||||
'coolify' => ['v4' => ['version' => '4.0.0'], 'helper' => ['version' => '1.0.19']],
|
||||
'stable' => ['coolify' => ['version' => '4.0.0'], 'helper' => ['version' => '1.0.25']],
|
||||
]),
|
||||
]);
|
||||
|
||||
refresh_versions_file();
|
||||
|
||||
expect(InstanceSettings::findOrFail(0)->helper_version)->toBe('1.0.19');
|
||||
expect(InstanceSettings::findOrFail(0)->helper_version)->toBe('1.0.25');
|
||||
});
|
||||
|
||||
it('does not use a stored helper version older than the bundled version', function () {
|
||||
@@ -58,7 +61,7 @@ it('keeps the stored helper version and does not notify when the CDN is unavaila
|
||||
InstanceSettings::findOrFail(0)->update(['helper_version' => '1.0.17']);
|
||||
Notification::fake();
|
||||
Sleep::fake();
|
||||
File::partialMock()->shouldNotReceive('put');
|
||||
File::partialMock()->shouldNotReceive('replace');
|
||||
Http::preventStrayRequests();
|
||||
Http::fake([
|
||||
'https://cdn.example.com/coolify/versions.json' => Http::response('error code: 1200', 503, ['Retry-After' => '60']),
|
||||
|
||||
@@ -163,7 +163,7 @@ test('resend settings save is audited without the api key', function () {
|
||||
test('manual instance upgrade is audited', function () {
|
||||
Bus::fake();
|
||||
config()->set('constants.coolify.version', '4.0.0');
|
||||
Cache::put('coolify:versions:all', ['coolify' => ['v4' => ['version' => '4.0.1']]], 3600);
|
||||
Cache::put('coolify:versions', ['coolify' => ['version' => '4.0.1']], 3600);
|
||||
|
||||
Livewire::test(Upgrade::class)->call('upgrade');
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ beforeEach(function () {
|
||||
|
||||
Queue::fake();
|
||||
Cache::flush();
|
||||
Cache::put('coolify:versions:all', ['coolify' => ['sentinel' => ['version' => '1.0.2']]], 3600);
|
||||
Cache::put('coolify:versions', ['sentinel' => ['version' => '1.0.2']], 3600);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$this->server = Server::factory()->create(['team_id' => $user->teams()->first()->id]);
|
||||
|
||||
@@ -22,7 +22,7 @@ beforeEach(function () {
|
||||
config()->set('constants.ssh.max_retries', 1);
|
||||
config()->set('constants.ssh.mux_enabled', false);
|
||||
Http::preventStrayRequests();
|
||||
Cache::put('coolify:versions:all', ['coolify' => ['sentinel' => ['version' => '1.0.0']]], 3600);
|
||||
Cache::put('coolify:versions', ['sentinel' => ['version' => '1.0.0']], 3600);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$team = $user->teams()->first();
|
||||
@@ -110,7 +110,7 @@ it('does not restart a Sentinel whose health check is still starting', function
|
||||
|
||||
it('restarts a running Sentinel that stopped pushing, at most once per hour', function () {
|
||||
Cache::flush();
|
||||
Cache::put('coolify:versions:all', ['coolify' => ['sentinel' => ['version' => '1.0.0']]], 3600);
|
||||
Cache::put('coolify:versions', ['sentinel' => ['version' => '1.0.0']], 3600);
|
||||
$this->server->forceFill(['sentinel_updated_at' => now()->subMinutes(15), 'sentinel_waiting_since' => null])->save();
|
||||
fakeRemoteProcesses([
|
||||
'*docker ps*' => Process::result(output: 'running Up 2 hours (healthy)'),
|
||||
@@ -124,7 +124,7 @@ it('restarts a running Sentinel that stopped pushing, at most once per hour', fu
|
||||
|
||||
it('does not restart a Sentinel that started recently and has not pushed yet', function () {
|
||||
Cache::flush();
|
||||
Cache::put('coolify:versions:all', ['coolify' => ['sentinel' => ['version' => '1.0.0']]], 3600);
|
||||
Cache::put('coolify:versions', ['sentinel' => ['version' => '1.0.0']], 3600);
|
||||
$this->server->forceFill(['sentinel_updated_at' => now()->subDays(5), 'sentinel_waiting_since' => now()->subMinutes(3)])->save();
|
||||
fakeRemoteProcesses([
|
||||
'*docker ps*' => Process::result(output: 'running Up 3 minutes (healthy)'),
|
||||
|
||||
@@ -15,6 +15,10 @@ use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
afterEach(function () {
|
||||
@unlink(storage_path('app/versions.json'));
|
||||
});
|
||||
|
||||
beforeEach(function () {
|
||||
Queue::fake([CheckTraefikVersionJob::class]);
|
||||
config()->set('constants.coolify.version', '4.4.6');
|
||||
@@ -41,8 +45,7 @@ it('reports a failed CDN request instead of "No new version available"', functio
|
||||
});
|
||||
|
||||
it('reports a new version when the CDN has a newer version', function () {
|
||||
Http::fake(['*' => Http::response(['coolify' => ['v4' => ['version' => '4.4.7']]])]);
|
||||
File::partialMock()->shouldReceive('replace')->with(storage_path('app/versions.json'), Mockery::any())->once();
|
||||
Http::fake(['*' => Http::response(['stable' => ['coolify' => ['version' => '4.4.7']]])]);
|
||||
|
||||
Livewire::test(Updates::class)
|
||||
->call('checkManually')
|
||||
|
||||
@@ -17,7 +17,7 @@ use Livewire\Livewire;
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('shows patch and minor upgrade warnings on the first proxy render', function () {
|
||||
Cache::put('coolify:versions:all', [
|
||||
Cache::put('coolify:versions', [
|
||||
'traefik' => [
|
||||
'v3.7' => '3.7.13',
|
||||
'v3.6' => '3.6.25',
|
||||
@@ -50,7 +50,7 @@ it('shows patch and minor upgrade warnings on the first proxy render', function
|
||||
});
|
||||
|
||||
it('shows a warning from the saved image before version detection finishes', function () {
|
||||
Cache::put('coolify:versions:all', [
|
||||
Cache::put('coolify:versions', [
|
||||
'traefik' => ['v3.7' => '3.7.13', 'v3.6' => '3.6.25'],
|
||||
]);
|
||||
|
||||
@@ -96,7 +96,7 @@ it('does not treat an unapplied image as the running Traefik version', function
|
||||
});
|
||||
|
||||
it('ignores stale minor upgrade information for the detected Traefik version', function () {
|
||||
Cache::put('coolify:versions:all', [
|
||||
Cache::put('coolify:versions', [
|
||||
'traefik' => [
|
||||
'v3.7' => '3.7.8',
|
||||
'v3.6' => '3.6.23',
|
||||
@@ -121,7 +121,7 @@ it('ignores stale minor upgrade information for the detected Traefik version', f
|
||||
});
|
||||
|
||||
it('does not offer the Traefik branch already configured on a running proxy', function () {
|
||||
Cache::put('coolify:versions:all', [
|
||||
Cache::put('coolify:versions', [
|
||||
'traefik' => [
|
||||
'v3.7' => '3.7.8',
|
||||
'v3.6' => '3.6.23',
|
||||
@@ -154,7 +154,7 @@ YAML;
|
||||
});
|
||||
|
||||
it('still offers a newer Traefik branch than the configured image', function () {
|
||||
Cache::put('coolify:versions:all', [
|
||||
Cache::put('coolify:versions', [
|
||||
'traefik' => [
|
||||
'v3.7' => '3.7.8',
|
||||
'v3.6' => '3.6.23',
|
||||
@@ -321,7 +321,7 @@ it('clears Traefik version state when the proxy changes', function () {
|
||||
|
||||
it('cleans stale Traefik version state while selecting servers to check', function () {
|
||||
Bus::fake();
|
||||
Cache::put('coolify:versions:all', [
|
||||
Cache::put('coolify:versions', [
|
||||
'traefik' => ['v3.7' => '3.7.8'],
|
||||
]);
|
||||
|
||||
|
||||
@@ -17,12 +17,10 @@ it('initializes latest version during mount from cached versions data', function
|
||||
|
||||
Cache::shouldReceive('remember')
|
||||
->once()
|
||||
->with('coolify:versions:all', 3600, Mockery::type(Closure::class))
|
||||
->with('coolify:versions', 3600, Mockery::type(Closure::class))
|
||||
->andReturn([
|
||||
'coolify' => [
|
||||
'v4' => [
|
||||
'version' => '4.0.0-beta.999',
|
||||
],
|
||||
'version' => '4.0.0-beta.999',
|
||||
],
|
||||
]);
|
||||
|
||||
@@ -42,7 +40,7 @@ it('falls back to 0.0.0 during mount when cached versions data is unavailable',
|
||||
|
||||
Cache::shouldReceive('remember')
|
||||
->once()
|
||||
->with('coolify:versions:all', 3600, Mockery::type(Closure::class))
|
||||
->with('coolify:versions', 3600, Mockery::type(Closure::class))
|
||||
->andReturn(null);
|
||||
|
||||
Livewire::test(Upgrade::class)
|
||||
@@ -58,12 +56,10 @@ it('clears stale upgrade availability when current version already matches lates
|
||||
|
||||
Cache::shouldReceive('remember')
|
||||
->once()
|
||||
->with('coolify:versions:all', 3600, Mockery::type(Closure::class))
|
||||
->with('coolify:versions', 3600, Mockery::type(Closure::class))
|
||||
->andReturn([
|
||||
'coolify' => [
|
||||
'v4' => [
|
||||
'version' => '4.0.0-beta.999',
|
||||
],
|
||||
'version' => '4.0.0-beta.999',
|
||||
],
|
||||
]);
|
||||
|
||||
@@ -83,12 +79,10 @@ it('clears stale upgrade availability when current version is newer than cached
|
||||
|
||||
Cache::shouldReceive('remember')
|
||||
->once()
|
||||
->with('coolify:versions:all', 3600, Mockery::type(Closure::class))
|
||||
->with('coolify:versions', 3600, Mockery::type(Closure::class))
|
||||
->andReturn([
|
||||
'coolify' => [
|
||||
'v4' => [
|
||||
'version' => '4.0.0-beta.999',
|
||||
],
|
||||
'version' => '4.0.0-beta.999',
|
||||
],
|
||||
]);
|
||||
|
||||
|
||||
@@ -23,146 +23,51 @@ beforeEach(function () {
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
@unlink(storage_path('app/versions.json'));
|
||||
Mockery::close();
|
||||
});
|
||||
|
||||
it('uses max of CDN and cache versions', function () {
|
||||
// CDN has older version
|
||||
Http::fake([
|
||||
'*' => Http::response([
|
||||
'coolify' => ['v4' => ['version' => '4.0.0']],
|
||||
'traefik' => ['v3.5' => '3.5.6'],
|
||||
], 200),
|
||||
]);
|
||||
|
||||
// Cache has newer version
|
||||
File::shouldReceive('exists')
|
||||
->with(storage_path('app/versions.json'))
|
||||
->andReturn(true);
|
||||
|
||||
File::shouldReceive('get')
|
||||
->with(storage_path('app/versions.json'))
|
||||
->andReturn(json_encode(['coolify' => ['v4' => ['version' => '4.0.10']]]));
|
||||
|
||||
File::shouldReceive('replace')
|
||||
->once()
|
||||
->with(storage_path('app/versions.json'), Mockery::on(function ($json) {
|
||||
$data = json_decode($json, true);
|
||||
|
||||
// Should use cached version (4.0.10), not CDN version (4.0.0)
|
||||
return $data['coolify']['v4']['version'] === '4.0.10';
|
||||
}));
|
||||
|
||||
Cache::shouldReceive('forget')->once();
|
||||
|
||||
it('keeps the local versions.json when the CDN serves an older Coolify version', function () {
|
||||
Http::fake(['*' => Http::response(['stable' => ['coolify' => ['version' => '4.0.0'], 'traefik' => ['v3.5' => '3.5.6']]], 200)]);
|
||||
File::put(storage_path('app/versions.json'), json_encode(['stable' => ['coolify' => ['version' => '4.0.10']]]));
|
||||
config(['constants.coolify.version' => '4.0.5']);
|
||||
|
||||
// Mock instanceSettings function
|
||||
$this->app->instance('App\Models\InstanceSettings', function () {
|
||||
return $this->settings;
|
||||
});
|
||||
|
||||
$job = new CheckForUpdatesJob;
|
||||
$job->handle();
|
||||
});
|
||||
|
||||
it('never downgrades from current running version', function () {
|
||||
// CDN has older version
|
||||
Http::fake([
|
||||
'*' => Http::response([
|
||||
'coolify' => ['v4' => ['version' => '4.0.0']],
|
||||
'traefik' => ['v3.5' => '3.5.6'],
|
||||
], 200),
|
||||
]);
|
||||
|
||||
// Cache also has older version
|
||||
File::shouldReceive('exists')
|
||||
->with(storage_path('app/versions.json'))
|
||||
->andReturn(true);
|
||||
|
||||
File::shouldReceive('get')
|
||||
->with(storage_path('app/versions.json'))
|
||||
->andReturn(json_encode(['coolify' => ['v4' => ['version' => '4.0.5']]]));
|
||||
|
||||
File::shouldReceive('replace')
|
||||
->once()
|
||||
->with(storage_path('app/versions.json'), Mockery::on(function ($json) {
|
||||
$data = json_decode($json, true);
|
||||
|
||||
// Should use running version (4.0.10), not CDN (4.0.0) or cache (4.0.5)
|
||||
return $data['coolify']['v4']['version'] === '4.0.10';
|
||||
}));
|
||||
|
||||
Cache::shouldReceive('forget')->once();
|
||||
|
||||
// Running version is newest
|
||||
config(['constants.coolify.version' => '4.0.10']);
|
||||
|
||||
Log::shouldReceive('warning')
|
||||
->once()
|
||||
->with('CDN served older Coolify version than cache', Mockery::type('array'));
|
||||
|
||||
expect(refresh_versions_file())->toMatchArray(['coolify' => ['version' => '4.0.10']])
|
||||
->and(json_decode(File::get(storage_path('app/versions.json')), true))->toBe(['stable' => ['coolify' => ['version' => '4.0.10']]]);
|
||||
});
|
||||
|
||||
it('never offers a version below the running version', function () {
|
||||
Http::fake(['*' => Http::response(['stable' => ['coolify' => ['version' => '4.0.0']]], 200)]);
|
||||
config(['constants.coolify.version' => '4.0.10']);
|
||||
|
||||
Log::shouldReceive('warning')
|
||||
->once()
|
||||
->with('Version downgrade prevented in CheckForUpdatesJob', Mockery::type('array'));
|
||||
|
||||
$this->app->instance('App\Models\InstanceSettings', function () {
|
||||
return $this->settings;
|
||||
});
|
||||
|
||||
$job = new CheckForUpdatesJob;
|
||||
$job->handle();
|
||||
expect(data_get(refresh_versions_file(), 'coolify.version'))->toBe('4.0.10');
|
||||
});
|
||||
|
||||
it('preserves other component versions when preventing Coolify downgrade', function () {
|
||||
// CDN has older Coolify but newer Traefik
|
||||
it('keeps the CDN versions of the other components when it keeps the running Coolify version', function () {
|
||||
Http::fake([
|
||||
'*' => Http::response([
|
||||
'coolify' => ['v4' => ['version' => '4.0.0']],
|
||||
'traefik' => ['v3.6' => '3.6.2'],
|
||||
'sentinel' => ['version' => '1.0.5'],
|
||||
'stable' => [
|
||||
'coolify' => ['version' => '4.0.0'],
|
||||
'traefik' => ['v3.6' => '3.6.2'],
|
||||
'sentinel' => ['version' => '1.0.5'],
|
||||
],
|
||||
], 200),
|
||||
]);
|
||||
|
||||
File::shouldReceive('exists')->andReturn(true);
|
||||
File::shouldReceive('get')
|
||||
->andReturn(json_encode([
|
||||
'coolify' => ['v4' => ['version' => '4.0.5']],
|
||||
'traefik' => ['v3.5' => '3.5.6'],
|
||||
]));
|
||||
|
||||
File::shouldReceive('replace')
|
||||
->once()
|
||||
->with(storage_path('app/versions.json'), Mockery::on(function ($json) {
|
||||
$data = json_decode($json, true);
|
||||
// Coolify should use running version
|
||||
expect($data['coolify']['v4']['version'])->toBe('4.0.10');
|
||||
// Traefik should use CDN version (newer)
|
||||
expect($data['traefik']['v3.6'])->toBe('3.6.2');
|
||||
// Sentinel should use CDN version
|
||||
expect($data['sentinel']['version'])->toBe('1.0.5');
|
||||
|
||||
return true;
|
||||
}));
|
||||
|
||||
Cache::shouldReceive('forget')->once();
|
||||
|
||||
config(['constants.coolify.version' => '4.0.10']);
|
||||
|
||||
Log::shouldReceive('warning')
|
||||
->once()
|
||||
->with('CDN served older Coolify version than cache', Mockery::type('array'));
|
||||
|
||||
Log::shouldReceive('warning')
|
||||
->once()
|
||||
->with('Version downgrade prevented in CheckForUpdatesJob', Mockery::type('array'));
|
||||
|
||||
$this->app->instance('App\Models\InstanceSettings', function () {
|
||||
return $this->settings;
|
||||
});
|
||||
|
||||
$job = new CheckForUpdatesJob;
|
||||
$job->handle();
|
||||
expect(refresh_versions_file())->toMatchArray([
|
||||
'coolify' => ['version' => '4.0.10'],
|
||||
'traefik' => ['v3.6' => '3.6.2'],
|
||||
'sentinel' => ['version' => '1.0.5'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps the local versions.json when the CDN fetch fails', function () {
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
<?php
|
||||
|
||||
it('reads the Coolify and helper versions by key from the versions.json of the CDN in install.sh', function () {
|
||||
$versions = shell_exec("jq --indent 4 --arg version 4.6.0-beta.1 '{coolify: ({v4: {\$version}} + del(.traefik)), traefik}' ".escapeshellarg(dirname(__DIR__, 2).'/upstream.json'));
|
||||
$parse = implode("\n", preg_grep('/^LATEST_(HELPER_)?VERSION=\$\(echo "\$VERSIONS_JSON"/', file(dirname(__DIR__, 2).'/scripts/install.sh')));
|
||||
$output = shell_exec('VERSIONS_JSON='.escapeshellarg($versions).' bash -c '.escapeshellarg($parse."\necho \"\$LATEST_VERSION \$LATEST_HELPER_VERSION\""));
|
||||
$helper = json_decode(file_get_contents(dirname(__DIR__, 2).'/upstream.json'), true)['helper']['version'];
|
||||
it('reads the Coolify and helper versions of its channel from versions.json in install.sh', function (string $channel, string $version) {
|
||||
$root = dirname(__DIR__, 2);
|
||||
$upstream = json_decode(file_get_contents("{$root}/upstream.json"), true);
|
||||
$versions = json_encode([
|
||||
'stable' => ['coolify' => ['version' => '4.5.1']] + $upstream,
|
||||
'preview' => ['coolify' => ['version' => '4.6.0-beta.1']] + $upstream,
|
||||
'nightly' => ['coolify' => ['version' => '4.6.0-beta.1.20261027.1']] + $upstream,
|
||||
'coolify' => ['v4' => ['version' => '4.5.1']],
|
||||
], JSON_PRETTY_PRINT);
|
||||
$parse = implode("\n", preg_grep('/^(CHANNEL_VERSIONS|LATEST_(HELPER_)?VERSION)=\$\(echo "\$(VERSIONS_JSON|CHANNEL_VERSIONS)"/', file("{$root}/scripts/install.sh")));
|
||||
$output = shell_exec('CHANNEL='.escapeshellarg($channel).' VERSIONS_JSON='.escapeshellarg($versions).' bash -c '.escapeshellarg($parse."\necho \"\$LATEST_VERSION \$LATEST_HELPER_VERSION\""));
|
||||
|
||||
expect(trim($output))->toBe("4.6.0-beta.1 {$helper}");
|
||||
});
|
||||
expect(trim($output))->toBe("{$version} {$upstream['helper']['version']}");
|
||||
})->with([
|
||||
'stable' => ['stable', '4.5.1'],
|
||||
'preview' => ['preview', '4.6.0-beta.1'],
|
||||
'nightly' => ['nightly', '4.6.0-beta.1.20261027.1'],
|
||||
]);
|
||||
|
||||
it('builds the development helper image from the coolify-helper repository', function (string $file) {
|
||||
expect(file_get_contents(dirname(__DIR__, 2)."/{$file}"))->toContain('https://github.com/coollabsio/coolify-helper.git#main:src');
|
||||
@@ -44,13 +54,13 @@ it('guides issue authors to the correct contribution branch', function () {
|
||||
->toContain('Small fixes should target `main`; larger changes should target `next`');
|
||||
});
|
||||
|
||||
it('writes versions.json in the production image from upstream.json so version checks work right after an upgrade', function () {
|
||||
it('ships release.json and upstream.json in the production image so version checks work right after an upgrade', function () {
|
||||
$dockerfile = file_get_contents(dirname(__DIR__, 2).'/docker/production/Dockerfile');
|
||||
$dockerignore = file_get_contents(dirname(__DIR__, 2).'/.dockerignore');
|
||||
|
||||
expect($dockerfile)
|
||||
->toContain('COPY --chown=www-data:www-data release.json upstream.json ./')
|
||||
->toContain('upstream.json > versions.json')
|
||||
->not->toContain('versions.json')
|
||||
->and($dockerignore)
|
||||
->not->toMatch('/^\/?(release|upstream)\.json$/m');
|
||||
});
|
||||
@@ -10,7 +10,6 @@ use App\Models\User;
|
||||
use App\Services\CoolifyUpgradeStatus;
|
||||
use App\Support\RemoteProcessCommand;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Illuminate\Support\Once;
|
||||
@@ -37,7 +36,7 @@ beforeEach(function () {
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
File::delete(storage_path('app/versions.json'));
|
||||
@unlink(storage_path('app/versions.json'));
|
||||
});
|
||||
|
||||
function updateChannelTestUse(string $channel, ?string $customTag = null, ?string $preview = '4.6.0-beta.2', string $nightly = '4.6.0-beta.1.20261027.3'): void
|
||||
@@ -45,14 +44,11 @@ function updateChannelTestUse(string $channel, ?string $customTag = null, ?strin
|
||||
InstanceSettings::findOrFail(0)->update(['update_channel' => $channel, 'update_custom_tag' => $customTag]);
|
||||
Once::flush();
|
||||
Http::fake([
|
||||
'cdn.example.com/coolify/release.json' => Http::response(array_filter([
|
||||
'stable' => ['coolify' => '4.5.1'],
|
||||
'preview' => $preview ? ['coolify' => $preview] : null,
|
||||
'nightly' => ['coolify' => $nightly],
|
||||
'cdn.example.com/coolify/versions.json' => Http::response(array_filter([
|
||||
'stable' => ['coolify' => ['version' => '4.5.1']],
|
||||
'preview' => $preview ? ['coolify' => ['version' => $preview]] : null,
|
||||
'nightly' => ['coolify' => ['version' => $nightly]],
|
||||
])),
|
||||
'cdn.example.com/coolify/versions.json' => Http::response(['coolify' => ['v4' => ['version' => '4.5.1']]]),
|
||||
'cdn.example.com/coolify/preview/versions.json' => Http::response(['coolify' => ['v4' => ['version' => $preview]]]),
|
||||
'cdn.example.com/coolify/nightly/versions.json' => Http::response(['coolify' => ['v4' => ['version' => $nightly]]]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -60,7 +56,7 @@ it('follows the newest version of the channel and the more stable ones', functio
|
||||
updateChannelTestUse($channel);
|
||||
|
||||
expect(fetch_latest_versions())->toMatchArray([
|
||||
'versions' => ['coolify' => ['v4' => ['version' => $version]]],
|
||||
'versions' => ['coolify' => ['version' => $version]],
|
||||
'upgrade_script_url' => $upgradeScriptUrl,
|
||||
]);
|
||||
})->with([
|
||||
@@ -77,16 +73,16 @@ it('asks only the stable channel of the CDN on the stable channel', function ()
|
||||
Http::assertSentCount(1);
|
||||
});
|
||||
|
||||
it('skips a channel that release.json does not list yet', function () {
|
||||
it('skips a channel that versions.json does not list yet', function () {
|
||||
updateChannelTestUse('nightly', preview: null, nightly: '4.6.0-alpha.0.20261027.1');
|
||||
|
||||
expect(fetch_latest_versions(tries: 1)['upgrade_script_url'])->toBe('https://cdn.example.com/coolify/nightly/upgrade.sh');
|
||||
});
|
||||
|
||||
it('checks nothing when release.json does not answer', function () {
|
||||
it('checks nothing when versions.json does not answer', function () {
|
||||
InstanceSettings::findOrFail(0)->update(['update_channel' => 'nightly']);
|
||||
Once::flush();
|
||||
Http::fake(['cdn.example.com/coolify/release.json' => Http::response('', 404)]);
|
||||
Http::fake(['cdn.example.com/coolify/versions.json' => Http::response('', 404)]);
|
||||
|
||||
expect(fetch_latest_versions(tries: 1))->toBeNull();
|
||||
});
|
||||
@@ -168,12 +164,10 @@ it('keeps the nightlies of the running version after a switch to stable until it
|
||||
InstanceSettings::findOrFail(0)->update(['update_channel' => 'stable', 'previous_update_channel' => 'nightly']);
|
||||
Once::flush();
|
||||
Http::fake([
|
||||
'cdn.example.com/coolify/release.json' => Http::response(['stable' => ['coolify' => $stable], 'nightly' => ['coolify' => $nightly]]),
|
||||
'cdn.example.com/coolify/versions.json' => Http::response(['coolify' => ['v4' => ['version' => $stable]]]),
|
||||
'cdn.example.com/coolify/nightly/versions.json' => Http::response(['coolify' => ['v4' => ['version' => $nightly]]]),
|
||||
'cdn.example.com/coolify/versions.json' => Http::response(['stable' => ['coolify' => ['version' => $stable]], 'nightly' => ['coolify' => ['version' => $nightly]]]),
|
||||
]);
|
||||
|
||||
expect(data_get(fetch_latest_versions(), 'versions.coolify.v4.version'))->toBe($version);
|
||||
expect(data_get(fetch_latest_versions(), 'versions.coolify.version'))->toBe($version);
|
||||
})->with([
|
||||
'a newer nightly of 4.6.0' => ['4.5.1', '4.6.0-beta.2.20261028.1', '4.6.0-beta.2.20261028.1'],
|
||||
'the stable release of 4.6.0' => ['4.6.0', '4.7.0-alpha.0.20261101.1', '4.6.0'],
|
||||
@@ -208,22 +202,3 @@ it('follows the channel it left after a switch to a more stable channel', functi
|
||||
'stable to nightly' => ['stable', 'nightly', null],
|
||||
'nightly to a custom image' => ['nightly', 'custom', null],
|
||||
]);
|
||||
|
||||
it('replaces the versions of the old channel after a channel change', function () {
|
||||
config(['constants.coolify.version' => '4.5.1']);
|
||||
File::put(storage_path('app/versions.json'), json_encode(['coolify' => ['v4' => ['version' => '4.6.0-beta.2.20261027.1']]]));
|
||||
updateChannelTestUse('stable');
|
||||
|
||||
refresh_versions_file(tries: 1, channelChanged: true);
|
||||
|
||||
expect(data_get(json_decode(File::get(storage_path('app/versions.json')), true), 'coolify.v4.version'))->toBe('4.5.1');
|
||||
});
|
||||
|
||||
it('falls back to the versions of the image when the CDN fails after a channel change', function () {
|
||||
File::put(storage_path('app/versions.json'), json_encode(['coolify' => ['v4' => ['version' => '4.6.0-beta.2.20261027.1']]]));
|
||||
Http::fake(['*' => Http::response('', 500)]);
|
||||
|
||||
refresh_versions_file(tries: 1, channelChanged: true);
|
||||
|
||||
expect(File::exists(storage_path('app/versions.json')))->toBeFalse();
|
||||
});
|
||||
@@ -99,7 +99,7 @@ function updateCoolifyTestPrepareUpgrade(): void
|
||||
|
||||
Http::fake([
|
||||
'*' => Http::response([
|
||||
'coolify' => ['v4' => ['version' => '4.0.10']],
|
||||
'stable' => ['coolify' => ['version' => '4.0.10']],
|
||||
], 200),
|
||||
]);
|
||||
}
|
||||
@@ -123,7 +123,7 @@ it('validates cache against running version before fallback', function () {
|
||||
|
||||
// Mock cache returning older version
|
||||
Cache::shouldReceive('remember')
|
||||
->andReturn(['coolify' => ['v4' => ['version' => '4.0.5']]]);
|
||||
->andReturn(['coolify' => ['version' => '4.0.5']]);
|
||||
|
||||
config(['constants.coolify.version' => '4.0.10']);
|
||||
|
||||
@@ -150,7 +150,7 @@ it('uses validated cache when CDN fails and cache is newer', function () {
|
||||
|
||||
// Cache has newer version than current
|
||||
Cache::shouldReceive('remember')
|
||||
->andReturn(['coolify' => ['v4' => ['version' => '4.0.10']]]);
|
||||
->andReturn(['coolify' => ['version' => '4.0.10']]);
|
||||
|
||||
config(['constants.coolify.version' => '4.0.5']);
|
||||
|
||||
@@ -183,7 +183,7 @@ it('passes the saved registry URL to the upgrade script command', function () {
|
||||
|
||||
Http::fake([
|
||||
'*' => Http::response([
|
||||
'coolify' => ['v4' => ['version' => '4.0.10']],
|
||||
'stable' => ['coolify' => ['version' => '4.0.10']],
|
||||
], 200),
|
||||
]);
|
||||
|
||||
@@ -212,7 +212,7 @@ it('falls back to docker io for the upgrade script command when no registry is s
|
||||
|
||||
Http::fake([
|
||||
'*' => Http::response([
|
||||
'coolify' => ['v4' => ['version' => '4.0.10']],
|
||||
'stable' => ['coolify' => ['version' => '4.0.10']],
|
||||
], 200),
|
||||
]);
|
||||
|
||||
@@ -364,7 +364,7 @@ it('prevents downgrade even with manual update', function () {
|
||||
// CDN returns older version
|
||||
Http::fake([
|
||||
'*' => Http::response([
|
||||
'coolify' => ['v4' => ['version' => '4.0.0']],
|
||||
'stable' => ['coolify' => ['version' => '4.0.0']],
|
||||
], 200),
|
||||
]);
|
||||
|
||||
@@ -396,7 +396,7 @@ it('does not start a second upgrade while a recent upgrade status is in progress
|
||||
'constants.ssh.mux_enabled' => false,
|
||||
]);
|
||||
updateCoolifyTestCreateRootServerAndSettings();
|
||||
Http::fake(['*' => Http::response(['coolify' => ['v4' => ['version' => '4.0.10']]], 200)]);
|
||||
Http::fake(['*' => Http::response(['stable' => ['coolify' => ['version' => '4.0.10']]], 200)]);
|
||||
$action = updateCoolifyTestActionWithStatus('3|Pulling Docker images|'.now()->subMinutes(2)->toIso8601String());
|
||||
|
||||
expect(fn () => $action->handle(manual_update: true))
|
||||
@@ -414,7 +414,7 @@ it('starts an upgrade when the previous upgrade status is older than the lock ex
|
||||
'constants.ssh.mux_enabled' => false,
|
||||
]);
|
||||
updateCoolifyTestCreateRootServerAndSettings();
|
||||
Http::fake(['*' => Http::response(['coolify' => ['v4' => ['version' => '4.0.10']]], 200)]);
|
||||
Http::fake(['*' => Http::response(['stable' => ['coolify' => ['version' => '4.0.10']]], 200)]);
|
||||
updateCoolifyTestActionWithStatus('3|Pulling Docker images|'.now()->subMinutes(16)->toIso8601String())
|
||||
->handle(manual_update: true);
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ uses(RefreshDatabase::class);
|
||||
it('keeps the update badge inside the sidebar brand area', function (string $version) {
|
||||
config()->set('constants.coolify.version', $version);
|
||||
seedBrowserResourceStack(['instanceSettings' => ['new_version_available' => true]]);
|
||||
Cache::put('coolify:versions:all', ['coolify' => ['v4' => ['version' => '9.9.9']]], 3600);
|
||||
Cache::put('coolify:versions', ['coolify' => ['version' => '9.9.9']], 3600);
|
||||
|
||||
$page = visit('/login')
|
||||
->fill('email', 'test@example.com')
|
||||
|
||||
Reference in new issue
Block a user