feat(install): add --preview and --nightly flags to install.sh

- Once Coolify is healthy, install.sh switches the instance to that update channel with php artisan app:update-channel
- The command and the update page share the rule that keeps the updates of the old channel after a switch to a more stable one
This commit is contained in:
peaklabs-dev
2026-11-02 00:07:20 +01:00
committed by GitHub
parent 0e30950793
commit bcd165a909
6 changed files with 112 additions and 20 deletions
+43
View File
@@ -0,0 +1,43 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class UpdateChannel extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:update-channel {channel : stable, preview or nightly}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Switch the update channel of the instance, as the update page does';
/**
* Execute the console command.
*/
public function handle(): int
{
$channel = $this->argument('channel');
if (! in_array($channel, ['stable', 'preview', 'nightly'], true)) {
$this->error("{$channel} is not an update channel, expected stable, preview or nightly");
return self::FAILURE;
}
$settings = instanceSettings();
$settings->switchUpdateChannel($channel);
$settings->save();
invalidate_versions_cache();
$this->info("The instance follows the {$channel} channel");
return self::SUCCESS;
}
}
+1 -20
View File
@@ -73,13 +73,10 @@ class Updates extends Component
$this->settings->update_check_frequency = $this->update_check_frequency;
$this->settings->is_auto_update_enabled = $this->is_auto_update_enabled;
$this->settings->docker_registry_url = $validated['docker_registry_url'];
$this->settings->update_channel = $validated['update_channel'];
$this->settings->switchUpdateChannel($validated['update_channel']);
$this->settings->update_custom_tag = $validated['update_custom_tag'];
$this->syncRegistryUrlToEnv($validated['docker_registry_url']);
$channelChanged = $this->settings->isDirty(['update_channel', 'update_custom_tag']);
if ($this->settings->isDirty('update_channel')) {
$this->settings->previous_update_channel = $this->channelFollowedUntilRelease($this->settings->getOriginal('update_channel'), $validated['update_channel']);
}
$changedFields = auditChangedFields($this->settings);
$this->settings->save();
if ($channelChanged) {
@@ -103,22 +100,6 @@ class Updates extends Component
}
}
/**
* After a switch to a more stable channel, the instance keeps the updates of the channel it left until the stable release of its version is out.
*/
private function channelFollowedUntilRelease(?string $from, string $to): ?string
{
$channels = ['stable', 'preview', 'nightly'];
$fromRank = array_search($from, $channels, true);
$toRank = array_search($to, $channels, true);
if ($fromRank === false || $toRank === false || $toRank >= $fromRank) {
return null;
}
$followed = $this->settings->getOriginal('previous_update_channel');
return array_search($followed, $channels, true) > $fromRank ? $followed : $from;
}
protected function syncRegistryUrlToEnv(string $registryUrl): void
{
if (! $this->server) {
+22
View File
@@ -146,6 +146,28 @@ class InstanceSettings extends Model
return random_int(0, 59).' 0 * * *';
}
/**
* After a switch to a more stable channel, the instance keeps the updates of the channel it left until the stable release of its version is out.
*/
public function switchUpdateChannel(string $channel): void
{
$from = $this->getOriginal('update_channel');
$this->update_channel = $channel;
if ($channel === $from) {
return;
}
$channels = ['stable', 'preview', 'nightly'];
$fromRank = array_search($from, $channels, true);
$toRank = array_search($channel, $channels, true);
$followed = $this->getOriginal('previous_update_channel');
$this->previous_update_channel = match (true) {
$fromRank === false || $toRank === false || $toRank >= $fromRank => null,
array_search($followed, $channels, true) > $fromRank => $followed,
default => $from,
};
}
public function isPasswordRegistrationAllowed(): bool
{
if (! $this->is_registration_enabled) {
+15
View File
@@ -11,12 +11,22 @@
## AUTOUPDATE - Set to "false" to disable auto-updates
## REGISTRY_URL - Custom registry URL for Docker images (default: docker.io)
## Arguments that can be passed:
## --preview or --nightly - Install the newest version of the preview or nightly channel and switch the instance to that update channel
set -e # Exit immediately if a command exits with a non-zero status
## $1 could be empty, so we need to disable this check
#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"
case "$1" in
--preview | --nightly)
CHANNEL="${1#--}"
CDN="https://cdn.coollabs.io/coolify/$CHANNEL"
shift
;;
esac
DATE=$(date +"%Y%m%d-%H%M%S")
OS_TYPE=$(grep -w "ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
@@ -1131,6 +1141,11 @@ if [ "$HEALTH" != "healthy" ]; then
fi
step_done
if [ "$CHANNEL" != "stable" ]; then
docker exec coolify php artisan app:update-channel "$CHANNEL" || warn "Could not switch the instance to the $CHANNEL update channel, switch it on the update page"
ui_flush_warnings
fi
if [ "$UI_TTY" = true ]; then
printf '%s\n' "$(ui_bar 100 "done")" >&3
fi
@@ -19,6 +19,17 @@ it('reads the Coolify and helper versions of its channel from versions.json in i
'nightly' => ['nightly', '4.6.0-beta.1.20261027.1'],
]);
it('selects the channel from the --preview or --nightly flag in install.sh', function (array $arguments, string $selection) {
preg_match('/^CDN=.*?^esac$/ms', file_get_contents(dirname(__DIR__, 2).'/scripts/install.sh'), $channelSelection);
$output = shell_exec('bash -c '.escapeshellarg($channelSelection[0]."\necho \"\$CHANNEL \$CDN \$1\"").' install.sh '.implode(' ', array_map('escapeshellarg', $arguments)));
expect(trim($output))->toBe($selection);
})->with([
'stable' => [[], 'stable https://cdn.coollabs.io/coolify'],
'preview' => [['--preview'], 'preview https://cdn.coollabs.io/coolify/preview'],
'nightly with a version' => [['--nightly', '4.6.0-beta.1.20261027.1'], 'nightly https://cdn.coollabs.io/coolify/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');
})->with(['scripts/dev-helper', 'docker-compose.dev.yml', 'docker-compose.dev-multi.yml']);
+20
View File
@@ -202,3 +202,23 @@ 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('switches the update channel with app:update-channel like the update page does', function (string $from, string $to, ?string $followed) {
InstanceSettings::findOrFail(0)->update(['update_channel' => $from]);
Once::flush();
$this->artisan('app:update-channel', ['channel' => $to])->assertSuccessful();
expect(InstanceSettings::findOrFail(0))
->update_channel->toBe($to)
->previous_update_channel->toBe($followed);
})->with([
'stable to nightly' => ['stable', 'nightly', null],
'nightly to preview' => ['nightly', 'preview', 'nightly'],
]);
it('refuses an update channel that does not exist', function () {
$this->artisan('app:update-channel', ['channel' => 'custom'])->assertFailed();
expect(InstanceSettings::findOrFail(0)->update_channel)->toBe('stable');
});