Merge remote-tracking branch 'origin/next' into feat/railpack

This commit is contained in:
Andras Bacsai committed 2026-04-09 17:12:26 +02:00
commit f573ad28a0
494 files changed
+24899 -3243

No files matched your search

+30 -15
View File
@@ -3,11 +3,14 @@
namespace App\Livewire\Project\Application;
use App\Actions\Application\GenerateConfig;
use App\Jobs\ApplicationDeploymentJob;
use App\Models\Application;
use App\Support\ValidationPatterns;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Collection;
use Livewire\Component;
use Livewire\Features\SupportEvents\Event;
use Spatie\Url\Url;
use Visus\Cuid2\Cuid2;
@@ -143,15 +146,15 @@ class General extends Component
'gitRepository' => 'required',
'gitBranch' => 'required',
'gitCommitSha' => ['nullable', 'string', 'regex:/^[a-zA-Z0-9][a-zA-Z0-9._\-\/]*$/'],
'installCommand' => 'nullable',
'buildCommand' => 'nullable',
'startCommand' => 'nullable',
'installCommand' => ValidationPatterns::shellSafeCommandRules(),
'buildCommand' => ValidationPatterns::shellSafeCommandRules(),
'startCommand' => ValidationPatterns::shellSafeCommandRules(),
'buildPack' => 'required',
'staticImage' => 'required',
'baseDirectory' => array_merge(['required'], array_slice(ValidationPatterns::directoryPathRules(), 1)),
'publishDirectory' => ValidationPatterns::directoryPathRules(),
'portsExposes' => 'required',
'portsMappings' => 'nullable',
'portsExposes' => ['required', 'string', 'regex:/^(\d+)(,\d+)*$/'],
'portsMappings' => ValidationPatterns::portMappingRules(),
'customNetworkAliases' => 'nullable',
'dockerfile' => 'nullable',
'dockerRegistryImageName' => 'nullable',
@@ -194,9 +197,12 @@ class General extends Component
'baseDirectory.regex' => 'The base directory must be a valid path starting with / and containing only safe characters.',
'publishDirectory.regex' => 'The publish directory must be a valid path starting with / and containing only safe characters.',
'dockerfileTargetBuild.regex' => 'The Dockerfile target build must contain only alphanumeric characters, dots, hyphens, and underscores.',
'dockerComposeCustomStartCommand.regex' => 'The Docker Compose start command contains invalid characters. Shell operators like ;, &, |, $, and backticks are not allowed.',
'dockerComposeCustomBuildCommand.regex' => 'The Docker Compose build command contains invalid characters. Shell operators like ;, &, |, $, and backticks are not allowed.',
'customDockerRunOptions.regex' => 'The custom Docker run options contain invalid characters. Shell operators like ;, &, |, $, and backticks are not allowed.',
'dockerComposeCustomStartCommand.regex' => 'The Docker Compose start command contains invalid characters. Shell operators like ;, |, $, and backticks are not allowed.',
'dockerComposeCustomBuildCommand.regex' => 'The Docker Compose build command contains invalid characters. Shell operators like ;, |, $, and backticks are not allowed.',
'customDockerRunOptions.regex' => 'The custom Docker run options contain invalid characters. Shell operators like ;, |, $, and backticks are not allowed.',
'installCommand.regex' => 'The install command contains invalid characters. Shell operators like ;, |, $, and backticks are not allowed.',
'buildCommand.regex' => 'The build command contains invalid characters. Shell operators like ;, |, $, and backticks are not allowed.',
'startCommand.regex' => 'The start command contains invalid characters. Shell operators like ;, |, $, and backticks are not allowed.',
'preDeploymentCommandContainer.regex' => 'The pre-deployment command container name must contain only alphanumeric characters, dots, hyphens, and underscores.',
'postDeploymentCommandContainer.regex' => 'The post-deployment command container name must contain only alphanumeric characters, dots, hyphens, and underscores.',
'name.required' => 'The Name field is required.',
@@ -206,6 +212,8 @@ class General extends Component
'staticImage.required' => 'The Static Image field is required.',
'baseDirectory.required' => 'The Base Directory field is required.',
'portsExposes.required' => 'The Exposed Ports field is required.',
'portsExposes.regex' => 'Ports exposes must be a comma-separated list of port numbers (e.g. 3000,3001).',
...ValidationPatterns::portMappingMessages(),
'isStatic.required' => 'The Static setting is required.',
'isStatic.boolean' => 'The Static setting must be true or false.',
'isSpa.required' => 'The SPA setting is required.',
@@ -288,7 +296,7 @@ class General extends Component
$this->authorize('update', $this->application);
$this->application->fqdn = null;
$this->application->settings->save();
} catch (\Illuminate\Auth\Access\AuthorizationException $e) {
} catch (AuthorizationException $e) {
// User doesn't have update permission, just continue without saving
}
}
@@ -309,7 +317,7 @@ class General extends Component
$this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n");
$this->application->custom_labels = base64_encode($this->customLabels);
$this->application->save();
} catch (\Illuminate\Auth\Access\AuthorizationException $e) {
} catch (AuthorizationException $e) {
// User doesn't have update permission, just use existing labels
// $this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n");
}
@@ -321,7 +329,7 @@ class General extends Component
$this->authorize('update', $this->application);
$this->initLoadingCompose = true;
$this->dispatch('info', 'Loading docker compose file.');
} catch (\Illuminate\Auth\Access\AuthorizationException $e) {
} catch (AuthorizationException $e) {
// User doesn't have update permission, skip loading compose file
}
}
@@ -587,7 +595,7 @@ class General extends Component
// Check if user has permission to update
try {
$this->authorize('update', $this->application);
} catch (\Illuminate\Auth\Access\AuthorizationException $e) {
} catch (AuthorizationException $e) {
// User doesn't have permission, revert the change and return
$this->application->refresh();
$this->syncData();
@@ -612,7 +620,7 @@ class General extends Component
$this->fqdn = null;
$this->application->fqdn = null;
$this->application->settings->save();
} catch (\Illuminate\Auth\Access\AuthorizationException $e) {
} catch (AuthorizationException $e) {
// User doesn't have update permission, just continue without saving
}
}
@@ -729,6 +737,7 @@ class General extends Component
$this->authorize('update', $this->application);
try {
$this->application->redirect = $this->redirect;
$has_www = collect($this->application->fqdns)->filter(fn ($fqdn) => str($fqdn)->contains('www.'))->count();
if ($has_www === 0 && $this->application->redirect === 'www') {
$this->dispatch('error', 'You want to redirect to www, but you do not have a www domain set.<br><br>Please add www to your domain list and as an A DNS record (if applicable).');
@@ -749,6 +758,12 @@ class General extends Component
$this->authorize('update', $this->application);
$this->resetErrorBag();
$this->portsExposes = str($this->portsExposes)->replace(' ', '')->trim()->toString();
if ($this->portsMappings) {
$this->portsMappings = str($this->portsMappings)->replace(' ', '')->trim()->toString();
}
$this->validate();
$oldPortsExposes = $this->application->ports_exposes;
@@ -809,7 +824,7 @@ class General extends Component
restoreBaseDirectory: $oldBaseDirectory,
restoreDockerComposeLocation: $oldDockerComposeLocation
);
if ($compose_return instanceof \Livewire\Features\SupportEvents\Event) {
if ($compose_return instanceof Event) {
// Validation failed - restore original values to component properties
$this->baseDirectory = $oldBaseDirectory;
$this->dockerComposeLocation = $oldDockerComposeLocation;
@@ -939,7 +954,7 @@ class General extends Component
$command = injectDockerComposeFlags(
$this->dockerComposeCustomBuildCommand,
".{$normalizedBase}{$this->dockerComposeLocation}",
\App\Jobs\ApplicationDeploymentJob::BUILD_TIME_ENV_PATH
ApplicationDeploymentJob::BUILD_TIME_ENV_PATH
);
// Inject build args if not using build secrets
+69 -11
View File
@@ -35,8 +35,17 @@ class Previews extends Component
public array $previewFqdns = [];
public array $previewDockerTags = [];
public ?int $manualPullRequestId = null;
public ?string $manualDockerTag = null;
protected $rules = [
'previewFqdns.*' => 'string|nullable',
'previewDockerTags.*' => 'string|nullable',
'manualPullRequestId' => 'integer|min:1|nullable',
'manualDockerTag' => 'string|nullable',
];
public function mount()
@@ -53,12 +62,17 @@ class Previews extends Component
$preview = $this->application->previews->get($key);
if ($preview) {
$preview->fqdn = $fqdn;
if ($this->application->build_pack === 'dockerimage') {
$preview->docker_registry_image_tag = $this->previewDockerTags[$key] ?? null;
}
}
}
} else {
$this->previewFqdns = [];
$this->previewDockerTags = [];
foreach ($this->application->previews as $key => $preview) {
$this->previewFqdns[$key] = $preview->fqdn;
$this->previewDockerTags[$key] = $preview->docker_registry_image_tag;
}
}
}
@@ -174,7 +188,7 @@ class Previews extends Component
}
}
public function add(int $pull_request_id, ?string $pull_request_html_url = null)
public function add(int $pull_request_id, ?string $pull_request_html_url = null, ?string $docker_registry_image_tag = null)
{
try {
$this->authorize('update', $this->application);
@@ -195,13 +209,18 @@ class Previews extends Component
} else {
$this->setDeploymentUuid();
$found = ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found && ! is_null($pull_request_html_url)) {
if (! $found && (! is_null($pull_request_html_url) || ($this->application->build_pack === 'dockerimage' && str($docker_registry_image_tag)->isNotEmpty()))) {
$found = ApplicationPreview::create([
'application_id' => $this->application->id,
'pull_request_id' => $pull_request_id,
'pull_request_html_url' => $pull_request_html_url,
'pull_request_html_url' => $pull_request_html_url ?? '',
'docker_registry_image_tag' => $docker_registry_image_tag,
]);
}
if ($found && $this->application->build_pack === 'dockerimage' && str($docker_registry_image_tag)->isNotEmpty()) {
$found->docker_registry_image_tag = $docker_registry_image_tag;
$found->save();
}
$found->generate_preview_fqdn();
$this->application->refresh();
$this->syncData(false);
@@ -217,37 +236,50 @@ class Previews extends Component
{
$this->authorize('deploy', $this->application);
$this->deploy($pull_request_id, $pull_request_html_url, force_rebuild: true);
$dockerRegistryImageTag = null;
if ($this->application->build_pack === 'dockerimage') {
$dockerRegistryImageTag = $this->application->previews()
->where('pull_request_id', $pull_request_id)
->value('docker_registry_image_tag');
}
$this->deploy($pull_request_id, $pull_request_html_url, force_rebuild: true, docker_registry_image_tag: $dockerRegistryImageTag);
}
public function add_and_deploy(int $pull_request_id, ?string $pull_request_html_url = null)
public function add_and_deploy(int $pull_request_id, ?string $pull_request_html_url = null, ?string $docker_registry_image_tag = null)
{
$this->authorize('deploy', $this->application);
$this->add($pull_request_id, $pull_request_html_url);
$this->deploy($pull_request_id, $pull_request_html_url);
$this->add($pull_request_id, $pull_request_html_url, $docker_registry_image_tag);
$this->deploy($pull_request_id, $pull_request_html_url, force_rebuild: false, docker_registry_image_tag: $docker_registry_image_tag);
}
public function deploy(int $pull_request_id, ?string $pull_request_html_url = null, bool $force_rebuild = false)
public function deploy(int $pull_request_id, ?string $pull_request_html_url = null, bool $force_rebuild = false, ?string $docker_registry_image_tag = null)
{
$this->authorize('deploy', $this->application);
try {
$this->setDeploymentUuid();
$found = ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found && ! is_null($pull_request_html_url)) {
ApplicationPreview::create([
if (! $found && (! is_null($pull_request_html_url) || ($this->application->build_pack === 'dockerimage' && str($docker_registry_image_tag)->isNotEmpty()))) {
$found = ApplicationPreview::create([
'application_id' => $this->application->id,
'pull_request_id' => $pull_request_id,
'pull_request_html_url' => $pull_request_html_url,
'pull_request_html_url' => $pull_request_html_url ?? '',
'docker_registry_image_tag' => $docker_registry_image_tag,
]);
}
if ($found && $this->application->build_pack === 'dockerimage' && str($docker_registry_image_tag)->isNotEmpty()) {
$found->docker_registry_image_tag = $docker_registry_image_tag;
$found->save();
}
$result = queue_application_deployment(
application: $this->application,
deployment_uuid: $this->deployment_uuid,
force_rebuild: $force_rebuild,
pull_request_id: $pull_request_id,
git_type: $found->git_type ?? null,
docker_registry_image_tag: $docker_registry_image_tag,
);
if ($result['status'] === 'queue_full') {
$this->dispatch('error', 'Deployment queue full', $result['message']);
@@ -277,6 +309,32 @@ class Previews extends Component
$this->parameters['deployment_uuid'] = $this->deployment_uuid;
}
public function addDockerImagePreview()
{
$this->authorize('deploy', $this->application);
$this->validateOnly('manualPullRequestId');
$this->validateOnly('manualDockerTag');
if ($this->application->build_pack !== 'dockerimage') {
$this->dispatch('error', 'Manual Docker Image previews are only available for Docker Image applications.');
return;
}
if ($this->manualPullRequestId === null || str($this->manualDockerTag)->isEmpty()) {
$this->dispatch('error', 'Both pull request id and docker tag are required.');
return;
}
$dockerTag = str($this->manualDockerTag)->trim()->value();
$this->add_and_deploy($this->manualPullRequestId, null, $dockerTag);
$this->manualPullRequestId = null;
$this->manualDockerTag = null;
}
private function stopContainers(array $containers, $server)
{
$containersToStop = collect($containers)->pluck('Names')->toArray();