Merge remote-tracking branch 'origin/main' into next

This commit is contained in:
github-actions[bot]
2026-10-05 19:47:56 +00:00
66 changed files with 2981 additions and 270 deletions
+25 -6
View File
@@ -116,9 +116,15 @@ function queue_application_deployment(Application $application, string $deployme
}
if ($admission['started']) {
ApplicationDeploymentJob::dispatch(
application_deployment_queue_id: $deployment->id,
);
try {
ApplicationDeploymentJob::dispatch(
application_deployment_queue_id: $deployment->id,
);
} catch (Throwable $exception) {
fail_undispatchable_deployment($deployment, $exception);
throw $exception;
}
}
return [
@@ -136,10 +142,23 @@ function force_start_deployment(ApplicationDeploymentQueue $deployment): bool
{
return start_queued_deployment($deployment, force: true);
}
function queue_next_deployment(Application $application)
/**
* Start the queued deployments that can run now on the application's primary server, on the
* server of the deployment that just ended (an additional server), and on every other server
* with a queued deployment of this application, because those waited for this one to end.
*/
function queue_next_deployment(Application $application, ?int $finished_deployment_server_id = null)
{
$server_id = $application->destination->server_id;
$queued_deployments = ApplicationDeploymentQueue::where('server_id', $server_id)
$application_queued_server_ids = ApplicationDeploymentQueue::where('application_id', $application->id)
->where('status', ApplicationDeploymentStatus::QUEUED)
->distinct()
->pluck('server_id');
$server_ids = collect([$application->destination->server_id, $finished_deployment_server_id])
->merge($application_queued_server_ids)
->filter(fn ($server_id) => $server_id !== null)
->unique()
->values();
$queued_deployments = ApplicationDeploymentQueue::whereIn('server_id', $server_ids)
->where('status', ApplicationDeploymentStatus::QUEUED)
->get()
->sortBy('created_at');
+29 -12
View File
@@ -342,8 +342,9 @@ function isComposeExternalVolume(mixed $declaration): bool
/**
* Returns the top-level declaration of a volume source when the Compose file declares it as
* external, and null for all other sources. The parsers use an external volume as written: no
* "{uuid}_" prefix, no preview suffix and no LocalPersistentVolume row, so that Coolify never
* removes a volume that it does not own.
* "{uuid}_" prefix and no LocalPersistentVolume row, so that Coolify never removes a volume that it
* does not own. A preview deployment never uses an external volume; it gets its own volume
* "{uuid}_{volume}-pr-{id}" like any other named volume.
*
* @param iterable<array-key, mixed> $topLevelVolumes
* @return array<string, mixed>|null
@@ -364,9 +365,11 @@ function composeExternalVolumeDeclaration(iterable $topLevelVolumes, string $sou
/**
* Tells if a parser uses the volume source as written because it is an external volume.
*
* Only for production deployments: a preview deployment always gets its own volume.
*
* Before Coolify used external volumes as written, the parsers renamed them like all other
* volumes (the old name, for example "{uuid}_{volume}" or "{uuid}_{volume}-pr-{id}"), so the
* resource wrote its data into the renamed volume. When the owner still has the storage entry
* volumes (the old name, for example "{uuid}_{volume}"), so the resource wrote its data into the
* renamed volume. When the owner still has the storage entry
* with the old name, the parser must keep the old name, or the resource loses its data. Then
* this function records a warning on the resource and returns false. When the user deletes that
* storage entry, the next parse uses the external volume.
@@ -431,7 +434,8 @@ function legacyApplicationRenamedVolumeDeclaration(string $name): array
* Records a warning when a legacy Compose application (parser version 1 or 2) does not use an
* external volume as written. These parsers keep the old volume name (see
* legacyApplicationComposeVolumeName()), so the resource keeps its data. The parser version 1 keeps
* the name of a production volume, so it uses the external volume and gets no warning.
* the name of a production volume, so it uses the external volume and gets no warning. A preview
* always uses its own volume, so it gets no warning either.
*
* The volume names are not validated here, so that the legacy parsers keep their old behavior. The
* warning shows the Docker volume name only when it is a literal, valid Docker volume name.
@@ -440,6 +444,9 @@ function legacyApplicationRenamedVolumeDeclaration(string $name): array
*/
function warnLegacyApplicationComposeExternalVolume(Application $resource, iterable $topLevelVolumes, string $source, int $pull_request_id): void
{
if ($pull_request_id !== 0) {
return;
}
$declaration = collect($topLevelVolumes)->get($source);
if (! isComposeExternalVolume($declaration)) {
return;
@@ -1331,6 +1338,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
}
// Parse the rest of the services
$previewOwnExternalVolumes = collect([]);
foreach ($services as $serviceName => $service) {
$image = data_get_str($service, 'image');
$restart = data_get_str($service, 'restart', RESTART_MODE);
@@ -1481,16 +1489,21 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
}
}
} elseif ($type->value() === 'volume') {
$legacyName = "{$uuid}_".Str::slug($source, '-');
if ($isPullRequest) {
$legacyName = addPreviewDeploymentSuffix($legacyName, $pull_request_id);
}
if (useComposeExternalVolumeAsWritten($resource, $originalResource, $topLevel->get('volumes'), $source->value(), $legacyName)) {
// Previews share the external volume. It gets no row, so Coolify never removes it.
// A preview never mounts an external volume: two deployments that write into the same
// data directory (for example two databases) can corrupt it. The preview gets its own
// volume with the name that older Coolify versions gave it, so it keeps its data, and
// the preview cleanup removes it.
$isPreviewOwnExternalVolume = $isPullRequest
&& composeExternalVolumeDeclaration($topLevel->get('volumes'), $source->value()) !== null;
if (! $isPullRequest && useComposeExternalVolumeAsWritten($resource, $originalResource, $topLevel->get('volumes'), $source->value(), "{$uuid}_".Str::slug($source, '-'))) {
// The external volume gets no row, so Coolify never removes it.
$volumesParsed->put($index, $volume);
continue;
}
if ($isPreviewOwnExternalVolume) {
$previewOwnExternalVolumes->put($source->value(), true);
}
$declaration = $topLevel->get('volumes')->get($source->value());
if (in_array(data_get($declaration, 'driver_opts.type'), ['cifs', 'nfs'], true)) {
// Network volumes are used as written.
@@ -1529,7 +1542,9 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
'resource_type' => get_class($originalResource),
]
);
$topLevel->get('volumes')->put($name, composeRenamedVolumeDeclarationFor($declaration, $name, $persistentVolume, $isPullRequest));
$topLevel->get('volumes')->put($name, $isPreviewOwnExternalVolume
? ['name' => $name]
: composeRenamedVolumeDeclarationFor($declaration, $name, $persistentVolume, $isPullRequest));
}
dispatch(new ServerFilesFromServerJob($originalResource));
$volumesParsed->put($index, $volume);
@@ -2134,6 +2149,8 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
$parsedServices->put($serviceName, $payload);
}
// The preview mounts its own volumes instead of these external volumes (see above), so it does not declare them.
$topLevel->put('volumes', $topLevel->get('volumes')->except($previewOwnExternalVolumes->keys()->all()));
$topLevel->put('services', $parsedServices);
$customOrder = ['services', 'volumes', 'networks', 'configs', 'secrets'];