From 51029aa8b97ade582c8aa126145d966e56afeb91 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 2 Nov 2026 13:58:15 +0100 Subject: [PATCH] fix(new-resource): accept a base directory without a leading slash Closes #12428 Co-Authored-By: Claude Opus 5.5 --- .../Project/New/GiteaPrivateRepository.php | 1 + .../Project/New/GithubPrivateRepository.php | 1 + .../New/GithubPrivateRepositoryDeployKey.php | 1 + .../Project/New/GitlabPrivateRepository.php | 1 + app/Livewire/Project/New/PublicGitRepository.php | 1 + app/Support/ValidationPatterns.php | 11 +++++++++++ .../new/gitea-private-repository.blade.php | 2 +- ...github-private-repository-deploy-key.blade.php | 2 +- .../new/github-private-repository.blade.php | 2 +- .../new/gitlab-private-repository.blade.php | 2 +- .../project/new/public-git-repository.blade.php | 2 +- ...loyKeyApplicationCreationAuthorizationTest.php | 15 +++++++++++++++ 12 files changed, 36 insertions(+), 5 deletions(-) diff --git a/app/Livewire/Project/New/GiteaPrivateRepository.php b/app/Livewire/Project/New/GiteaPrivateRepository.php index 9c1fb558c8..bcaba739f6 100644 --- a/app/Livewire/Project/New/GiteaPrivateRepository.php +++ b/app/Livewire/Project/New/GiteaPrivateRepository.php @@ -165,6 +165,7 @@ class GiteaPrivateRepository extends Component { try { $this->authorize('create', Application::class); + $this->base_directory = ValidationPatterns::normalizeDirectoryPath($this->base_directory); $validator = validator([ 'selected_repository_path' => $this->selected_repository_path, diff --git a/app/Livewire/Project/New/GithubPrivateRepository.php b/app/Livewire/Project/New/GithubPrivateRepository.php index dacfbe4881..e3dd5219dd 100644 --- a/app/Livewire/Project/New/GithubPrivateRepository.php +++ b/app/Livewire/Project/New/GithubPrivateRepository.php @@ -202,6 +202,7 @@ class GithubPrivateRepository extends Component { try { $this->authorize('create', Application::class); + $this->base_directory = ValidationPatterns::normalizeDirectoryPath($this->base_directory); // Validate git repository parts and branch $validator = validator([ diff --git a/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php b/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php index 2a74efc50b..200dd623b4 100644 --- a/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php +++ b/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php @@ -155,6 +155,7 @@ class GithubPrivateRepositoryDeployKey extends Component $privateKey = PrivateKey::ownedByCurrentTeam()->findOrFail($this->private_key_id); + $this->base_directory = ValidationPatterns::normalizeDirectoryPath($this->base_directory); $this->validate(); try { $destination_uuid = $this->query['destination'] ?? null; diff --git a/app/Livewire/Project/New/GitlabPrivateRepository.php b/app/Livewire/Project/New/GitlabPrivateRepository.php index a11fc69c01..9871c3af4c 100644 --- a/app/Livewire/Project/New/GitlabPrivateRepository.php +++ b/app/Livewire/Project/New/GitlabPrivateRepository.php @@ -162,6 +162,7 @@ class GitlabPrivateRepository extends Component { try { $this->authorize('create', Application::class); + $this->base_directory = ValidationPatterns::normalizeDirectoryPath($this->base_directory); $validator = validator([ 'selected_repository_path' => $this->selected_repository_path, diff --git a/app/Livewire/Project/New/PublicGitRepository.php b/app/Livewire/Project/New/PublicGitRepository.php index 3ea109b4a0..349b431e43 100644 --- a/app/Livewire/Project/New/PublicGitRepository.php +++ b/app/Livewire/Project/New/PublicGitRepository.php @@ -278,6 +278,7 @@ class PublicGitRepository extends Component { try { $this->authorize('create', Application::class); + $this->base_directory = ValidationPatterns::normalizeDirectoryPath($this->base_directory); $this->validate(); diff --git a/app/Support/ValidationPatterns.php b/app/Support/ValidationPatterns.php index 77c89f2fbe..1aa46ddabd 100644 --- a/app/Support/ValidationPatterns.php +++ b/app/Support/ValidationPatterns.php @@ -576,6 +576,17 @@ class ValidationPatterns return ['nullable', 'string', 'max:'.$maxLength, 'regex:'.self::DIRECTORY_PATH_PATTERN]; } + /** + * Normalize a typed directory path to the form that DIRECTORY_PATH_PATTERN expects: + * "apps/web/" becomes "/apps/web", and an empty value becomes "/". + */ + public static function normalizeDirectoryPath(?string $path): string + { + $path = trim(trim((string) $path), '/'); + + return '/'.$path; + } + /** * Get validation rules for Docker build target fields */ diff --git a/resources/views/livewire/project/new/gitea-private-repository.blade.php b/resources/views/livewire/project/new/gitea-private-repository.blade.php index 6ed15d6312..da18da3cf7 100644 --- a/resources/views/livewire/project/new/gitea-private-repository.blade.php +++ b/resources/views/livewire/project/new/gitea-private-repository.blade.php @@ -135,7 +135,7 @@ file="docker_compose_location" fileLabel="Compose file" defaultFile="/docker-compose.yaml" /> @else - @endif diff --git a/resources/views/livewire/project/new/github-private-repository-deploy-key.blade.php b/resources/views/livewire/project/new/github-private-repository-deploy-key.blade.php index 6ee5dfe990..313cc5c919 100644 --- a/resources/views/livewire/project/new/github-private-repository-deploy-key.blade.php +++ b/resources/views/livewire/project/new/github-private-repository-deploy-key.blade.php @@ -85,7 +85,7 @@ file="docker_compose_location" fileLabel="Compose file" defaultFile="/docker-compose.yaml" /> @else - @endif diff --git a/resources/views/livewire/project/new/github-private-repository.blade.php b/resources/views/livewire/project/new/github-private-repository.blade.php index c89472dd7a..431a124175 100644 --- a/resources/views/livewire/project/new/github-private-repository.blade.php +++ b/resources/views/livewire/project/new/github-private-repository.blade.php @@ -162,7 +162,7 @@ file="docker_compose_location" fileLabel="Compose file" defaultFile="/docker-compose.yaml" /> @else - @endif diff --git a/resources/views/livewire/project/new/gitlab-private-repository.blade.php b/resources/views/livewire/project/new/gitlab-private-repository.blade.php index 0a0e7e152d..064c28d5a6 100644 --- a/resources/views/livewire/project/new/gitlab-private-repository.blade.php +++ b/resources/views/livewire/project/new/gitlab-private-repository.blade.php @@ -139,7 +139,7 @@ file="docker_compose_location" fileLabel="Compose file" defaultFile="/docker-compose.yaml" /> @else - @endif diff --git a/resources/views/livewire/project/new/public-git-repository.blade.php b/resources/views/livewire/project/new/public-git-repository.blade.php index 663e267272..39c186636e 100644 --- a/resources/views/livewire/project/new/public-git-repository.blade.php +++ b/resources/views/livewire/project/new/public-git-repository.blade.php @@ -111,7 +111,7 @@ file="docker_compose_location" fileLabel="Compose file" defaultFile="/docker-compose.yaml" /> @else - @endif diff --git a/tests/Feature/Authorization/DeployKeyApplicationCreationAuthorizationTest.php b/tests/Feature/Authorization/DeployKeyApplicationCreationAuthorizationTest.php index 4dc81bc4b4..eb86f1a293 100644 --- a/tests/Feature/Authorization/DeployKeyApplicationCreationAuthorizationTest.php +++ b/tests/Feature/Authorization/DeployKeyApplicationCreationAuthorizationTest.php @@ -128,6 +128,21 @@ test('deploy key application keeps the base directory for every build pack', fun expect(Application::query()->sole()->base_directory)->toBe('/apps/api'); })->with(['dockerfile', 'railpack', 'static', 'dockercompose']); +test('deploy key application accepts a base directory without a leading slash', function (string $typed) { + actAsDeployKeyUser($this->admin, $this->team); + + deployKeyComponent() + ->call('setPrivateKey', $this->privateKey->id) + ->set('repository_url', 'git@example.com:owner/monorepo.git') + ->set('branch', 'main') + ->set('build_pack', 'dockerfile') + ->set('base_directory', $typed) + ->call('submit') + ->assertHasNoErrors(); + + expect(Application::query()->sole()->base_directory)->toBe('/apps/api'); +})->with(['apps/api', 'apps/api/', ' /apps/api/ ']); + test('deploy key application uses the repository root when the base directory is empty', function () { actAsDeployKeyUser($this->admin, $this->team);