diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 5dced0599..44e489976 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -652,11 +652,32 @@ private function deploy_docker_compose_buildpack() $this->save_buildtime_environment_variables(); if ($this->docker_compose_custom_build_command) { - // Prepend DOCKER_BUILDKIT=1 if BuildKit is supported $build_command = $this->docker_compose_custom_build_command; + + // Inject --env-file flag if not already present in custom command + // This ensures build-time environment variables are available during the build + if (! str_contains($build_command, '--env-file')) { + $build_command = str_replace( + 'docker compose', + 'docker compose --env-file /artifacts/build-time.env', + $build_command + ); + } + + // Prepend DOCKER_BUILDKIT=1 if BuildKit is supported if ($this->dockerBuildkitSupported) { $build_command = "DOCKER_BUILDKIT=1 {$build_command}"; } + + // Append build arguments if not using build secrets (matching default behavior) + if (! $this->application->settings->use_build_secrets && $this->build_args instanceof \Illuminate\Support\Collection && $this->build_args->isNotEmpty()) { + $build_args_string = $this->build_args->implode(' '); + // Escape single quotes for bash -c context used by executeInDocker + $build_args_string = str_replace("'", "'\\''", $build_args_string); + $build_command .= " {$build_args_string}"; + $this->application_deployment_queue->addLogEntry('Adding build arguments to custom Docker Compose build command.'); + } + $this->execute_remote_command( [executeInDocker($this->deployment_uuid, "cd {$this->basedir} && {$build_command}"), 'hidden' => true], ); diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php index c95260efe..415a1d378 100644 --- a/resources/views/livewire/project/application/general.blade.php +++ b/resources/views/livewire/project/application/general.blade.php @@ -259,7 +259,7 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
toBe('docker compose --env-file /artifacts/build-time.env -f ./docker-compose.yaml build'); + expect($customCommand)->toContain('--env-file /artifacts/build-time.env'); +}); + +it('does not duplicate --env-file flag when already present', function () { + $customCommand = 'docker compose --env-file /custom/.env -f ./docker-compose.yaml build'; + + // Simulate the injection logic from ApplicationDeploymentJob + if (! str_contains($customCommand, '--env-file')) { + $customCommand = str_replace( + 'docker compose', + 'docker compose --env-file /artifacts/build-time.env', + $customCommand + ); + } + + expect($customCommand)->toBe('docker compose --env-file /custom/.env -f ./docker-compose.yaml build'); + expect(substr_count($customCommand, '--env-file'))->toBe(1); +}); + +it('preserves custom build command structure with env-file injection', function () { + $customCommand = 'docker compose -f ./custom/path/docker-compose.prod.yaml build --no-cache'; + + // Simulate the injection logic from ApplicationDeploymentJob + if (! str_contains($customCommand, '--env-file')) { + $customCommand = str_replace( + 'docker compose', + 'docker compose --env-file /artifacts/build-time.env', + $customCommand + ); + } + + expect($customCommand)->toBe('docker compose --env-file /artifacts/build-time.env -f ./custom/path/docker-compose.prod.yaml build --no-cache'); + expect($customCommand)->toContain('--env-file /artifacts/build-time.env'); + expect($customCommand)->toContain('-f ./custom/path/docker-compose.prod.yaml'); + expect($customCommand)->toContain('build --no-cache'); +}); + +it('handles multiple docker compose commands in custom build command', function () { + // Edge case: Only the first 'docker compose' should get the env-file flag + $customCommand = 'docker compose -f ./docker-compose.yaml build'; + + // Simulate the injection logic from ApplicationDeploymentJob + if (! str_contains($customCommand, '--env-file')) { + $customCommand = str_replace( + 'docker compose', + 'docker compose --env-file /artifacts/build-time.env', + $customCommand + ); + } + + // Note: str_replace replaces ALL occurrences, which is acceptable in this case + // since you typically only have one 'docker compose' command + expect($customCommand)->toContain('docker compose --env-file /artifacts/build-time.env'); +}); + +it('verifies build args would be appended correctly', function () { + $customCommand = 'docker compose --env-file /artifacts/build-time.env -f ./docker-compose.yaml build'; + $buildArgs = collect([ + '--build-arg NODE_ENV=production', + '--build-arg API_URL=https://api.example.com', + ]); + + // Simulate build args appending logic + $buildArgsString = $buildArgs->implode(' '); + $buildArgsString = str_replace("'", "'\\''", $buildArgsString); + $customCommand .= " {$buildArgsString}"; + + expect($customCommand)->toContain('--build-arg NODE_ENV=production'); + expect($customCommand)->toContain('--build-arg API_URL=https://api.example.com'); + expect($customCommand)->toBe( + 'docker compose --env-file /artifacts/build-time.env -f ./docker-compose.yaml build --build-arg NODE_ENV=production --build-arg API_URL=https://api.example.com' + ); +}); + +it('properly escapes single quotes in build args', function () { + $buildArg = "--build-arg MESSAGE='Hello World'"; + + // Simulate the escaping logic from ApplicationDeploymentJob + $escapedBuildArg = str_replace("'", "'\\''", $buildArg); + + expect($escapedBuildArg)->toBe("--build-arg MESSAGE='\\''Hello World'\\''"); +}); + +it('handles DOCKER_BUILDKIT prefix with env-file injection', function () { + $customCommand = 'docker compose -f ./docker-compose.yaml build'; + + // Simulate the injection logic from ApplicationDeploymentJob + if (! str_contains($customCommand, '--env-file')) { + $customCommand = str_replace( + 'docker compose', + 'docker compose --env-file /artifacts/build-time.env', + $customCommand + ); + } + + // Simulate BuildKit support + $dockerBuildkitSupported = true; + if ($dockerBuildkitSupported) { + $customCommand = "DOCKER_BUILDKIT=1 {$customCommand}"; + } + + expect($customCommand)->toBe('DOCKER_BUILDKIT=1 docker compose --env-file /artifacts/build-time.env -f ./docker-compose.yaml build'); + expect($customCommand)->toStartWith('DOCKER_BUILDKIT=1'); + expect($customCommand)->toContain('--env-file /artifacts/build-time.env'); +});