feat(validation): add custom validation rules for Git repository URLs and branches

- Introduced `ValidGitRepositoryUrl` and `ValidGitBranch` validation rules to ensure safe and valid input for Git repository URLs and branch names.
- Updated relevant Livewire components and API controllers to utilize the new validation rules, enhancing security against command injection and invalid inputs.
- Refactored existing validation logic to improve consistency and maintainability across the application.
This commit is contained in:
Andras Bacsai
2025-08-22 14:38:21 +02:00
parent 841e33bac0
commit 8408205955
9 changed files with 393 additions and 28 deletions

View File

@@ -15,6 +15,8 @@ use App\Models\PrivateKey;
use App\Models\Project;
use App\Models\Server;
use App\Models\Service;
use App\Rules\ValidGitBranch;
use App\Rules\ValidGitRepositoryUrl;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use OpenApi\Attributes as OA;
@@ -831,8 +833,8 @@ class ApplicationsController extends Controller
$destination = $destinations->first();
if ($type === 'public') {
$validationRules = [
'git_repository' => 'string|required',
'git_branch' => 'string|required',
'git_repository' => ['string', 'required', new ValidGitRepositoryUrl],
'git_branch' => ['string', 'required', new ValidGitBranch],
'build_pack' => ['required', Rule::enum(BuildPackTypes::class)],
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required',
'docker_compose_location' => 'string',
@@ -883,7 +885,7 @@ class ApplicationsController extends Controller
$application->source_type = GithubApp::class;
$application->source_id = GithubApp::find(0)->id;
}
$application->git_repository = $repository_url_parsed->getSegment(1).'/'.$repository_url_parsed->getSegment(2);
$application->git_repository = str($repository_url_parsed->getSegment(1).'/'.$repository_url_parsed->getSegment(2))->trim()->toString();
$application->fqdn = $fqdn;
$application->destination_id = $destination->id;
$application->destination_type = $destination->getMorphClass();
@@ -935,7 +937,7 @@ class ApplicationsController extends Controller
} elseif ($type === 'private-gh-app') {
$validationRules = [
'git_repository' => 'string|required',
'git_branch' => 'string|required',
'git_branch' => ['string', 'required', new ValidGitBranch],
'build_pack' => ['required', Rule::enum(BuildPackTypes::class)],
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required',
'github_app_uuid' => 'string|required',
@@ -1043,7 +1045,7 @@ class ApplicationsController extends Controller
$application->docker_compose_domains = $dockerComposeDomainsJson;
}
$application->fqdn = $fqdn;
$application->git_repository = $gitRepository;
$application->git_repository = str($gitRepository)->trim()->toString();
$application->destination_id = $destination->id;
$application->destination_type = $destination->getMorphClass();
$application->environment_id = $environment->id;
@@ -1090,8 +1092,8 @@ class ApplicationsController extends Controller
} elseif ($type === 'private-deploy-key') {
$validationRules = [
'git_repository' => 'string|required',
'git_branch' => 'string|required',
'git_repository' => ['string', 'required', new ValidGitRepositoryUrl],
'git_branch' => ['string', 'required', new ValidGitBranch],
'build_pack' => ['required', Rule::enum(BuildPackTypes::class)],
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required',
'private_key_uuid' => 'string|required',

View File

@@ -7,6 +7,7 @@ use App\Models\GithubApp;
use App\Models\Project;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use App\Rules\ValidGitBranch;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Route;
use Livewire\Component;
@@ -155,6 +156,21 @@ class GithubPrivateRepository extends Component
public function submit()
{
try {
// Validate git repository parts and branch
$validator = validator([
'selected_repository_owner' => $this->selected_repository_owner,
'selected_repository_repo' => $this->selected_repository_repo,
'selected_branch_name' => $this->selected_branch_name,
], [
'selected_repository_owner' => 'required|string|regex:/^[a-zA-Z0-9\-_]+$/',
'selected_repository_repo' => 'required|string|regex:/^[a-zA-Z0-9\-_\.]+$/',
'selected_branch_name' => ['required', 'string', new ValidGitBranch],
]);
if ($validator->fails()) {
throw new \RuntimeException('Invalid repository data: '.$validator->errors()->first());
}
$destination_uuid = $this->query['destination'];
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
if (! $destination) {
@@ -171,8 +187,8 @@ class GithubPrivateRepository extends Component
$application = Application::create([
'name' => generate_application_name($this->selected_repository_owner.'/'.$this->selected_repository_repo, $this->selected_branch_name),
'repository_project_id' => $this->selected_repository_id,
'git_repository' => "{$this->selected_repository_owner}/{$this->selected_repository_repo}",
'git_branch' => $this->selected_branch_name,
'git_repository' => str($this->selected_repository_owner)->trim()->toString().'/'.str($this->selected_repository_repo)->trim()->toString(),
'git_branch' => str($this->selected_branch_name)->trim()->toString(),
'build_pack' => $this->build_pack,
'ports_exposes' => $this->port,
'publish_directory' => $this->publish_directory,

View File

@@ -9,6 +9,8 @@ use App\Models\PrivateKey;
use App\Models\Project;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use App\Rules\ValidGitBranch;
use App\Rules\ValidGitRepositoryUrl;
use Illuminate\Support\Str;
use Livewire\Component;
use Spatie\Url\Url;
@@ -53,17 +55,29 @@ class GithubPrivateRepositoryDeployKey extends Component
private ?string $git_host = null;
private string $git_repository;
private ?string $git_repository = null;
protected $rules = [
'repository_url' => 'required',
'branch' => 'required|string',
'repository_url' => ['required', 'string'],
'branch' => ['required', 'string'],
'port' => 'required|numeric',
'is_static' => 'required|boolean',
'publish_directory' => 'nullable|string',
'build_pack' => 'required|string',
];
protected function rules()
{
return [
'repository_url' => ['required', 'string', new ValidGitRepositoryUrl],
'branch' => ['required', 'string', new ValidGitBranch],
'port' => 'required|numeric',
'is_static' => 'required|boolean',
'publish_directory' => 'nullable|string',
'build_pack' => 'required|string',
];
}
protected $validationAttributes = [
'repository_url' => 'Repository',
'branch' => 'Branch',
@@ -135,6 +149,9 @@ class GithubPrivateRepositoryDeployKey extends Component
$this->get_git_source();
// Note: git_repository has already been validated and transformed in get_git_source()
// It may now be in SSH format (git@host:repo.git) which is valid for deploy keys
$project = Project::where('uuid', $this->parameters['project_uuid'])->first();
$environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first();
if ($this->git_source === 'other') {
@@ -194,6 +211,15 @@ class GithubPrivateRepositoryDeployKey extends Component
private function get_git_source()
{
// Validate repository URL before parsing
$validator = validator(['repository_url' => $this->repository_url], [
'repository_url' => ['required', 'string', new ValidGitRepositoryUrl],
]);
if ($validator->fails()) {
throw new \RuntimeException('Invalid repository URL: '.$validator->errors()->first('repository_url'));
}
$this->repository_url_parsed = Url::fromString($this->repository_url);
$this->git_host = $this->repository_url_parsed->getHost();
$this->git_repository = $this->repository_url_parsed->getSegment(1).'/'.$this->repository_url_parsed->getSegment(2);
@@ -206,8 +232,10 @@ class GithubPrivateRepositoryDeployKey extends Component
if (str($this->repository_url)->startsWith('http')) {
$this->git_host = $this->repository_url_parsed->getHost();
$this->git_repository = $this->repository_url_parsed->getSegment(1).'/'.$this->repository_url_parsed->getSegment(2);
// Convert to SSH format for deploy key usage
$this->git_repository = Str::finish("git@$this->git_host:$this->git_repository", '.git');
} else {
// If it's already in SSH format, just use it as-is
$this->git_repository = $this->repository_url;
}
$this->git_source = 'other';

View File

@@ -9,6 +9,8 @@ use App\Models\Project;
use App\Models\Service;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use App\Rules\ValidGitBranch;
use App\Rules\ValidGitRepositoryUrl;
use Carbon\Carbon;
use Livewire\Component;
use Spatie\Url\Url;
@@ -62,7 +64,7 @@ class PublicGitRepository extends Component
public bool $new_compose_services = false;
protected $rules = [
'repository_url' => 'required|url',
'repository_url' => ['required', 'string'],
'port' => 'required|numeric',
'isStatic' => 'required|boolean',
'publish_directory' => 'nullable|string',
@@ -71,6 +73,20 @@ class PublicGitRepository extends Component
'docker_compose_location' => 'nullable|string',
];
protected function rules()
{
return [
'repository_url' => ['required', 'string', new ValidGitRepositoryUrl],
'port' => 'required|numeric',
'isStatic' => 'required|boolean',
'publish_directory' => 'nullable|string',
'build_pack' => 'required|string',
'base_directory' => 'nullable|string',
'docker_compose_location' => 'nullable|string',
'git_branch' => ['required', 'string', new ValidGitBranch],
];
}
protected $validationAttributes = [
'repository_url' => 'repository',
'port' => 'port',
@@ -141,6 +157,15 @@ class PublicGitRepository extends Component
public function loadBranch()
{
try {
// Validate repository URL
$validator = validator(['repository_url' => $this->repository_url], [
'repository_url' => ['required', 'string', new ValidGitRepositoryUrl],
]);
if ($validator->fails()) {
throw new \RuntimeException('Invalid repository URL: '.$validator->errors()->first('repository_url'));
}
if (str($this->repository_url)->startsWith('git@')) {
$github_instance = str($this->repository_url)->after('git@')->before(':');
$repository = str($this->repository_url)->after(':')->before('.git');
@@ -191,6 +216,15 @@ class PublicGitRepository extends Component
$this->git_branch = 'main';
$this->base_directory = '/';
// Validate repository URL before parsing
$validator = validator(['repository_url' => $this->repository_url], [
'repository_url' => ['required', 'string', new ValidGitRepositoryUrl],
]);
if ($validator->fails()) {
throw new \RuntimeException('Invalid repository URL: '.$validator->errors()->first('repository_url'));
}
$this->repository_url_parsed = Url::fromString($this->repository_url);
$this->git_host = $this->repository_url_parsed->getHost();
$this->git_repository = $this->repository_url_parsed->getSegment(1).'/'.$this->repository_url_parsed->getSegment(2);
@@ -234,6 +268,27 @@ class PublicGitRepository extends Component
{
try {
$this->validate();
// Additional validation for git repository and branch
if ($this->git_source === 'other') {
// For 'other' sources, git_repository contains the full URL
$validator = validator(['git_repository' => $this->git_repository], [
'git_repository' => ['required', 'string', new ValidGitRepositoryUrl],
]);
if ($validator->fails()) {
throw new \RuntimeException('Invalid repository URL: '.$validator->errors()->first('git_repository'));
}
}
$branchValidator = validator(['git_branch' => $this->git_branch], [
'git_branch' => ['required', 'string', new ValidGitBranch],
]);
if ($branchValidator->fails()) {
throw new \RuntimeException('Invalid branch: '.$branchValidator->errors()->first('git_branch'));
}
$destination_uuid = $this->query['destination'];
$project_uuid = $this->parameters['project_uuid'];
$environment_uuid = $this->parameters['environment_uuid'];

View File

@@ -1128,15 +1128,20 @@ class Application extends BaseModel
$branch = $this->git_branch;
['repository' => $customRepository, 'port' => $customPort] = $this->customRepository();
$baseDir = $custom_base_dir ?? $this->generateBaseDir($deployment_uuid);
// Escape shell arguments for safety to prevent command injection
$escapedBranch = escapeshellarg($branch);
$escapedBaseDir = escapeshellarg($baseDir);
$commands = collect([]);
// Check if shallow clone is enabled
$isShallowCloneEnabled = $this->settings?->is_git_shallow_clone_enabled ?? false;
$depthFlag = $isShallowCloneEnabled ? ' --depth=1' : '';
$git_clone_command = "git clone{$depthFlag} -b \"{$this->git_branch}\"";
$git_clone_command = "git clone{$depthFlag} -b {$escapedBranch}";
if ($only_checkout) {
$git_clone_command = "git clone{$depthFlag} --no-checkout -b \"{$this->git_branch}\"";
$git_clone_command = "git clone{$depthFlag} --no-checkout -b {$escapedBranch}";
}
if ($pull_request_id !== 0) {
$pr_branch_name = "pr-{$pull_request_id}-coolify";
@@ -1150,7 +1155,8 @@ class Application extends BaseModel
if ($this->source->getMorphClass() === \App\Models\GithubApp::class) {
if ($this->source->is_public) {
$fullRepoUrl = "{$this->source->html_url}/{$customRepository}";
$git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$customRepository} {$baseDir}";
$escapedRepoUrl = escapeshellarg("{$this->source->html_url}/{$customRepository}");
$git_clone_command = "{$git_clone_command} {$escapedRepoUrl} {$escapedBaseDir}";
if (! $only_checkout) {
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command, public: true);
}
@@ -1162,11 +1168,15 @@ class Application extends BaseModel
} else {
$github_access_token = generateGithubInstallationToken($this->source);
if ($exec_in_docker) {
$git_clone_command = "{$git_clone_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git {$baseDir}";
$fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git";
$repoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git";
$escapedRepoUrl = escapeshellarg($repoUrl);
$git_clone_command = "{$git_clone_command} {$escapedRepoUrl} {$escapedBaseDir}";
$fullRepoUrl = $repoUrl;
} else {
$git_clone_command = "{$git_clone_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository} {$baseDir}";
$fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}";
$repoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}";
$escapedRepoUrl = escapeshellarg($repoUrl);
$git_clone_command = "{$git_clone_command} {$escapedRepoUrl} {$escapedBaseDir}";
$fullRepoUrl = $repoUrl;
}
if (! $only_checkout) {
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command, public: false);
@@ -1181,10 +1191,11 @@ class Application extends BaseModel
$branch = "pull/{$pull_request_id}/head:$pr_branch_name";
$git_checkout_command = $this->buildGitCheckoutCommand($pr_branch_name);
$escapedPrBranch = escapeshellarg($branch);
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "cd {$baseDir} && git fetch origin {$branch} && $git_checkout_command"));
$commands->push(executeInDocker($deployment_uuid, "cd {$escapedBaseDir} && git fetch origin {$escapedPrBranch} && $git_checkout_command"));
} else {
$commands->push("cd {$baseDir} && git fetch origin {$branch} && $git_checkout_command");
$commands->push("cd {$escapedBaseDir} && git fetch origin {$escapedPrBranch} && $git_checkout_command");
}
}
@@ -1202,7 +1213,8 @@ class Application extends BaseModel
throw new RuntimeException('Private key not found. Please add a private key to the application and try again.');
}
$private_key = base64_encode($private_key);
$git_clone_command_base = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$customRepository} {$baseDir}";
$escapedCustomRepository = escapeshellarg($customRepository);
$git_clone_command_base = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$escapedCustomRepository} {$escapedBaseDir}";
if ($only_checkout) {
$git_clone_command = $git_clone_command_base;
} else {

View File

@@ -0,0 +1,96 @@
<?php
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\Log;
class ValidGitBranch implements ValidationRule
{
/**
* Run the validation rule.
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (empty($value)) {
return;
}
$branch = trim($value);
// Check for dangerous shell metacharacters
$dangerousChars = [
';', '|', '&', '$', '`', '(', ')', '{', '}',
'<', '>', '\n', '\r', '\0', '"', "'", '\\',
'!', '*', '?', '[', ']', '~', '^', ':', ' ',
'#',
];
foreach ($dangerousChars as $char) {
if (str_contains($branch, $char)) {
Log::warning('Git branch validation failed - dangerous character', [
'branch' => $branch,
'character' => $char,
'ip' => request()->ip(),
'user_id' => auth()->id(),
]);
$fail('The :attribute contains invalid characters.');
return;
}
}
// Git branch name rules:
// - Cannot contain: .., //, @{
// - Cannot start or end with: / or .
// - Cannot be empty after trimming
if (str_contains($branch, '..') ||
str_contains($branch, '//') ||
str_contains($branch, '@{')) {
$fail('The :attribute contains invalid patterns.');
return;
}
if (str_starts_with($branch, '/') ||
str_ends_with($branch, '/') ||
str_starts_with($branch, '.') ||
str_ends_with($branch, '.')) {
$fail('The :attribute cannot start or end with / or .');
return;
}
// Allow only safe characters for branch names
// Letters, numbers, hyphens, underscores, forward slashes, and dots
if (! preg_match('/^[a-zA-Z0-9\-_\/\.]+$/', $branch)) {
$fail('The :attribute contains invalid characters. Only letters, numbers, hyphens, underscores, forward slashes, and dots are allowed.');
return;
}
// Additional Git-specific validations
// Branch name cannot be 'HEAD'
if ($branch === 'HEAD') {
$fail('The :attribute cannot be HEAD.');
return;
}
// Check for consecutive dots (not allowed in Git)
if (str_contains($branch, '..')) {
$fail('The :attribute cannot contain consecutive dots.');
return;
}
// Check for .lock suffix (reserved by Git)
if (str_ends_with($branch, '.lock')) {
$fail('The :attribute cannot end with .lock.');
return;
}
}
}

View File

@@ -0,0 +1,157 @@
<?php
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\Log;
class ValidGitRepositoryUrl implements ValidationRule
{
protected bool $allowSSH;
protected bool $allowIP;
public function __construct(bool $allowSSH = true, bool $allowIP = false)
{
$this->allowSSH = $allowSSH;
$this->allowIP = $allowIP;
}
/**
* Run the validation rule.
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (empty($value)) {
return;
}
// Check for dangerous shell metacharacters that could be used for command injection
$dangerousChars = [
';', '|', '&', '$', '`', '(', ')', '{', '}',
'[', ']', '<', '>', '\n', '\r', '\0', '"', "'",
'\\', '!', '?', '*', '~', '^', '%', '=', '+',
'#', // Comment character that could hide commands
];
foreach ($dangerousChars as $char) {
if (str_contains($value, $char)) {
Log::warning('Git repository URL validation failed - dangerous character', [
'url' => $value,
'character' => $char,
'ip' => request()->ip(),
'user_id' => auth()->id(),
]);
$fail('The :attribute contains invalid characters.');
return;
}
}
// Check for command substitution patterns
$dangerousPatterns = [
'/\$\(.*\)/', // Command substitution $(...)
'/\${.*}/', // Variable expansion ${...}
'/;;/', // Double semicolon
'/&&/', // Command chaining
'/\|\|/', // Command chaining
'/>>/', // Redirect append
'/<</', // Here document
'/\\\n/', // Line continuation
'/\.\.[\/\\\\]/', // Directory traversal
];
foreach ($dangerousPatterns as $pattern) {
if (preg_match($pattern, $value)) {
Log::warning('Git repository URL validation failed - dangerous pattern', [
'url' => $value,
'pattern' => $pattern,
'ip' => request()->ip(),
'user_id' => auth()->id(),
]);
$fail('The :attribute contains invalid patterns.');
return;
}
}
// Validate based on URL type
if (str_starts_with($value, 'git@')) {
if (! $this->allowSSH) {
$fail('SSH URLs are not allowed.');
return;
}
// Validate SSH URL format (git@host:user/repo.git)
if (! preg_match('/^git@[a-zA-Z0-9\.\-]+:[a-zA-Z0-9\-_\/\.]+$/', $value)) {
$fail('The :attribute is not a valid SSH repository URL.');
return;
}
} elseif (str_starts_with($value, 'http://') || str_starts_with($value, 'https://')) {
// Validate HTTP(S) URL
if (! filter_var($value, FILTER_VALIDATE_URL)) {
$fail('The :attribute is not a valid URL.');
return;
}
$parsed = parse_url($value);
// Check for IP addresses if not allowed
if (! $this->allowIP && filter_var($parsed['host'] ?? '', FILTER_VALIDATE_IP)) {
Log::warning('Git repository URL contains IP address', [
'url' => $value,
'ip' => request()->ip(),
'user_id' => auth()->id(),
]);
$fail('The :attribute cannot use IP addresses.');
return;
}
// Check for localhost/internal addresses
$host = strtolower($parsed['host'] ?? '');
$internalHosts = ['localhost', '127.0.0.1', '0.0.0.0', '::1'];
if (in_array($host, $internalHosts) || str_ends_with($host, '.local')) {
Log::warning('Git repository URL points to internal host', [
'url' => $value,
'host' => $host,
'ip' => request()->ip(),
'user_id' => auth()->id(),
]);
$fail('The :attribute cannot point to internal hosts.');
return;
}
// Ensure no query parameters or fragments
if (! empty($parsed['query']) || ! empty($parsed['fragment'])) {
$fail('The :attribute should not contain query parameters or fragments.');
return;
}
// Validate path contains only safe characters
$path = $parsed['path'] ?? '';
if (! empty($path) && ! preg_match('/^[a-zA-Z0-9\-_\/\.]+$/', $path)) {
$fail('The :attribute path contains invalid characters.');
return;
}
} elseif (str_starts_with($value, 'git://')) {
// Validate git:// protocol URL
if (! preg_match('/^git:\/\/[a-zA-Z0-9\.\-]+\/[a-zA-Z0-9\-_\/\.]+$/', $value)) {
$fail('The :attribute is not a valid git:// URL.');
return;
}
} else {
$fail('The :attribute must start with https://, http://, git://, or git@.');
return;
}
}
}

View File

@@ -1,7 +1,7 @@
<div>
<h1>Create a new Application</h1>
<div class="pb-4">Deploy any public or private Git repositories through a Deploy Key.</div>
<div class="flex flex-col pt-4">
<div class="flex flex-col ">
@if ($current_step === 'private_keys')
<h2 class="pb-4">Select a private key</h2>
<div class="flex flex-col justify-center gap-2 text-left ">
@@ -46,9 +46,8 @@
</div>
@endif
@if ($current_step === 'repository')
<h2 class="pb-4">Select a repository</h2>
<form class="flex flex-col gap-2 pt-2" wire:submit='submit'>
<x-forms.input id="repository_url" required label="Repository Url (https:// or git@)" />
<form class="flex flex-col gap-2" wire:submit='submit'>
<x-forms.input id="repository_url" required label="Repository URL (https:// or git@)" />
<div class="flex gap-2">
<x-forms.input id="branch" required label="Branch" />
<x-forms.select wire:model.live="build_pack" label="Build Pack" required>

View File

@@ -1,6 +1,6 @@
<div x-data x-init="$nextTick(() => { if ($refs.autofocusInput) $refs.autofocusInput.focus(); })">
<h1>Create a new Application</h1>
<div class="pb-4">Deploy any public Git repositories.</div>
<div class="pb-8">Deploy any public Git repositories.</div>
<!-- Repository URL Form -->
<form class="flex flex-col gap-2" wire:submit='loadBranch'>