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);