diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 503366e5d..297585562 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -41,7 +41,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, EnvironmentVariableAnalyzer, ExecuteRemoteCommand, InteractsWithQueue, Queueable, SerializesModels; - private const BUILD_TIME_ENV_PATH = '/artifacts/build-time.env'; + public const BUILD_TIME_ENV_PATH = '/artifacts/build-time.env'; private const BUILD_SCRIPT_PATH = '/artifacts/build.sh'; diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 5817d2883..71ca9720e 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -1012,12 +1012,16 @@ public function getDockerComposeBuildCommandPreviewProperty(): string return ''; } + // Normalize baseDirectory to prevent double slashes (e.g., when baseDirectory is '/') + $normalizedBase = $this->baseDirectory === '/' ? '' : rtrim($this->baseDirectory, '/'); + // Use relative path for clarity in preview (e.g., ./backend/docker-compose.yaml) // Actual deployment uses absolute path: /artifacts/{deployment_uuid}{base_directory}{docker_compose_location} + // Build-time env path references ApplicationDeploymentJob::BUILD_TIME_ENV_PATH as source of truth return injectDockerComposeFlags( $this->dockerComposeCustomBuildCommand, - ".{$this->baseDirectory}{$this->dockerComposeLocation}", - '/artifacts/build-time.env' + ".{$normalizedBase}{$this->dockerComposeLocation}", + \App\Jobs\ApplicationDeploymentJob::BUILD_TIME_ENV_PATH ); } @@ -1027,11 +1031,14 @@ public function getDockerComposeStartCommandPreviewProperty(): string return ''; } + // Normalize baseDirectory to prevent double slashes (e.g., when baseDirectory is '/') + $normalizedBase = $this->baseDirectory === '/' ? '' : rtrim($this->baseDirectory, '/'); + // Use relative path for clarity in preview (e.g., ./backend/docker-compose.yaml) // Placeholder {workdir}/.env shows it's the workdir .env file (runtime env, not build-time) return injectDockerComposeFlags( $this->dockerComposeCustomStartCommand, - ".{$this->baseDirectory}{$this->dockerComposeLocation}", + ".{$normalizedBase}{$this->dockerComposeLocation}", '{workdir}/.env' ); } diff --git a/tests/Unit/Livewire/ApplicationGeneralPreviewTest.php b/tests/Unit/Livewire/ApplicationGeneralPreviewTest.php new file mode 100644 index 000000000..cea05a998 --- /dev/null +++ b/tests/Unit/Livewire/ApplicationGeneralPreviewTest.php @@ -0,0 +1,156 @@ +makePartial(); + $component->baseDirectory = '/'; + $component->dockerComposeLocation = '/docker-compose.yaml'; + $component->dockerComposeCustomBuildCommand = 'docker compose build'; + + $preview = $component->getDockerComposeBuildCommandPreviewProperty(); + + // Should be ./docker-compose.yaml, NOT .//docker-compose.yaml + expect($preview) + ->toBeString() + ->toContain('./docker-compose.yaml') + ->not->toContain('.//'); +}); + +it('correctly formats build command preview with nested baseDirectory', function () { + $component = Mockery::mock(General::class)->makePartial(); + $component->baseDirectory = '/backend'; + $component->dockerComposeLocation = '/docker-compose.yaml'; + $component->dockerComposeCustomBuildCommand = 'docker compose build'; + + $preview = $component->getDockerComposeBuildCommandPreviewProperty(); + + // Should be ./backend/docker-compose.yaml + expect($preview) + ->toBeString() + ->toContain('./backend/docker-compose.yaml'); +}); + +it('correctly formats build command preview with deeply nested baseDirectory', function () { + $component = Mockery::mock(General::class)->makePartial(); + $component->baseDirectory = '/apps/api/backend'; + $component->dockerComposeLocation = '/docker-compose.prod.yaml'; + $component->dockerComposeCustomBuildCommand = 'docker compose build'; + + $preview = $component->getDockerComposeBuildCommandPreviewProperty(); + + expect($preview) + ->toBeString() + ->toContain('./apps/api/backend/docker-compose.prod.yaml'); +}); + +it('uses BUILD_TIME_ENV_PATH constant instead of hardcoded path in build command preview', function () { + $component = Mockery::mock(General::class)->makePartial(); + $component->baseDirectory = '/'; + $component->dockerComposeLocation = '/docker-compose.yaml'; + $component->dockerComposeCustomBuildCommand = 'docker compose build'; + + $preview = $component->getDockerComposeBuildCommandPreviewProperty(); + + // Should contain the path from the constant + expect($preview) + ->toBeString() + ->toContain(ApplicationDeploymentJob::BUILD_TIME_ENV_PATH); +}); + +it('returns empty string for build command preview when no custom build command is set', function () { + $component = Mockery::mock(General::class)->makePartial(); + $component->baseDirectory = '/backend'; + $component->dockerComposeLocation = '/docker-compose.yaml'; + $component->dockerComposeCustomBuildCommand = null; + + $preview = $component->getDockerComposeBuildCommandPreviewProperty(); + + expect($preview)->toBe(''); +}); + +it('prevents double slashes in start command preview when baseDirectory is root', function () { + $component = Mockery::mock(General::class)->makePartial(); + $component->baseDirectory = '/'; + $component->dockerComposeLocation = '/docker-compose.yaml'; + $component->dockerComposeCustomStartCommand = 'docker compose up -d'; + + $preview = $component->getDockerComposeStartCommandPreviewProperty(); + + // Should be ./docker-compose.yaml, NOT .//docker-compose.yaml + expect($preview) + ->toBeString() + ->toContain('./docker-compose.yaml') + ->not->toContain('.//'); +}); + +it('correctly formats start command preview with nested baseDirectory', function () { + $component = Mockery::mock(General::class)->makePartial(); + $component->baseDirectory = '/frontend'; + $component->dockerComposeLocation = '/compose.yaml'; + $component->dockerComposeCustomStartCommand = 'docker compose up -d'; + + $preview = $component->getDockerComposeStartCommandPreviewProperty(); + + expect($preview) + ->toBeString() + ->toContain('./frontend/compose.yaml'); +}); + +it('uses workdir env placeholder in start command preview', function () { + $component = Mockery::mock(General::class)->makePartial(); + $component->baseDirectory = '/'; + $component->dockerComposeLocation = '/docker-compose.yaml'; + $component->dockerComposeCustomStartCommand = 'docker compose up -d'; + + $preview = $component->getDockerComposeStartCommandPreviewProperty(); + + // Start command should use {workdir}/.env, not build-time env + expect($preview) + ->toBeString() + ->toContain('{workdir}/.env') + ->not->toContain(ApplicationDeploymentJob::BUILD_TIME_ENV_PATH); +}); + +it('returns empty string for start command preview when no custom start command is set', function () { + $component = Mockery::mock(General::class)->makePartial(); + $component->baseDirectory = '/backend'; + $component->dockerComposeLocation = '/docker-compose.yaml'; + $component->dockerComposeCustomStartCommand = null; + + $preview = $component->getDockerComposeStartCommandPreviewProperty(); + + expect($preview)->toBe(''); +}); + +it('handles baseDirectory with trailing slash correctly in build command', function () { + $component = Mockery::mock(General::class)->makePartial(); + $component->baseDirectory = '/backend/'; + $component->dockerComposeLocation = '/docker-compose.yaml'; + $component->dockerComposeCustomBuildCommand = 'docker compose build'; + + $preview = $component->getDockerComposeBuildCommandPreviewProperty(); + + // rtrim should remove trailing slash to prevent double slashes + expect($preview) + ->toBeString() + ->toContain('./backend/docker-compose.yaml') + ->not->toContain('backend//'); +}); + +it('handles baseDirectory with trailing slash correctly in start command', function () { + $component = Mockery::mock(General::class)->makePartial(); + $component->baseDirectory = '/backend/'; + $component->dockerComposeLocation = '/docker-compose.yaml'; + $component->dockerComposeCustomStartCommand = 'docker compose up -d'; + + $preview = $component->getDockerComposeStartCommandPreviewProperty(); + + // rtrim should remove trailing slash to prevent double slashes + expect($preview) + ->toBeString() + ->toContain('./backend/docker-compose.yaml') + ->not->toContain('backend//'); +});