diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 896e28666..b3dae9f41 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -36,7 +36,6 @@ use Symfony\Component\Yaml\Yaml; use Throwable; use Visus\Cuid2\Cuid2; -use Yosymfony\Toml\Toml; class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue { @@ -89,6 +88,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue private bool $is_this_additional_server = false; + private bool $is_laravel_or_symfony = false; + private ?ApplicationPreview $preview = null; private ?string $git_type = null; @@ -772,6 +773,7 @@ private function deploy_nixpacks_buildpack() } } $this->clone_repository(); + $this->detect_laravel_symfony(); $this->cleanup_git(); $this->generate_nixpacks_confs(); $this->generate_compose_file(); @@ -1286,71 +1288,23 @@ private function elixir_finetunes() } } - private function laravel_finetunes() - { - if ($this->pull_request_id === 0) { - $envType = 'environment_variables'; - } else { - $envType = 'environment_variables_preview'; - } - $nixpacks_php_fallback_path = $this->application->{$envType}->where('key', 'NIXPACKS_PHP_FALLBACK_PATH')->first(); - $nixpacks_php_root_dir = $this->application->{$envType}->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first(); - - if (! $nixpacks_php_fallback_path) { - $nixpacks_php_fallback_path = new EnvironmentVariable; - $nixpacks_php_fallback_path->key = 'NIXPACKS_PHP_FALLBACK_PATH'; - $nixpacks_php_fallback_path->value = '/index.php'; - $nixpacks_php_fallback_path->resourceable_id = $this->application->id; - $nixpacks_php_fallback_path->resourceable_type = 'App\Models\Application'; - $nixpacks_php_fallback_path->save(); - } - if (! $nixpacks_php_root_dir) { - $nixpacks_php_root_dir = new EnvironmentVariable; - $nixpacks_php_root_dir->key = 'NIXPACKS_PHP_ROOT_DIR'; - $nixpacks_php_root_dir->value = '/app/public'; - $nixpacks_php_root_dir->resourceable_id = $this->application->id; - $nixpacks_php_root_dir->resourceable_type = 'App\Models\Application'; - $nixpacks_php_root_dir->save(); - } - - return [$nixpacks_php_fallback_path, $nixpacks_php_root_dir]; - } - - private function php_finetunes(&$parsed) + private function symfony_finetunes(&$parsed) { $installCmds = data_get($parsed, 'phases.install.cmds', []); + $variables = data_get($parsed, 'variables', []); - $hasComposerInstall = false; - foreach ($installCmds as $cmd) { - if (str_contains($cmd, 'composer install') || str_contains($cmd, 'composer update')) { - $hasComposerInstall = true; - break; - } + $envCommands = []; + foreach (array_keys($variables) as $key) { + $envCommands[] = "printf '%s=%s\\n' ".escapeshellarg($key)." \"\${$key}\" >> /app/.env"; } - if ($hasComposerInstall) { - $variables = data_get($parsed, 'variables', []); + if (! empty($envCommands)) { + $createEnvCmd = 'touch /app/.env'; - $envCommands = []; - foreach (array_keys($variables) as $key) { - $envCommands[] = "printf '%s=%s\\n' ".escapeshellarg($key)." \"\${$key}\" >> /app/.env"; - } + array_unshift($installCmds, $createEnvCmd); + array_splice($installCmds, 1, 0, $envCommands); - if (! empty($envCommands)) { - $checkSymfonyCmd = 'if [ -f /app/composer.json ] && (grep -q "symfony/dotenv\\|symfony/framework-bundle\\|symfony/flex" /app/composer.json 2>/dev/null); then touch /app/.env; fi'; - - $conditionalEnvCommands = []; - foreach ($envCommands as $envCmd) { - $conditionalEnvCommands[] = 'if [ -f /app/.env ]; then '.$envCmd.'; fi'; - } - - array_unshift($installCmds, $checkSymfonyCmd); - array_splice($installCmds, 1, 0, $conditionalEnvCommands); - - data_set($parsed, 'phases.install.cmds', $installCmds); - - $this->application_deployment_queue->addLogEntry('Symfony app detected: Added conditional .env file creation for Symfony apps'); - } + data_set($parsed, 'phases.install.cmds', $installCmds); } } @@ -1506,6 +1460,7 @@ private function deploy_pull_request() $this->prepare_builder_image(); $this->check_git_if_build_needed(); $this->clone_repository(); + $this->detect_laravel_symfony(); $this->cleanup_git(); if ($this->application->build_pack === 'nixpacks') { $this->generate_nixpacks_confs(); @@ -1772,6 +1727,89 @@ private function generate_git_import_commands() return $commands; } + private function detect_laravel_symfony() + { + if ($this->application->build_pack !== 'nixpacks') { + return; + } + + $this->execute_remote_command([ + executeInDocker($this->deployment_uuid, "test -f {$this->workdir}/composer.json && echo 'exists' || echo 'not-exists'"), + 'save' => 'composer_json_exists', + 'hidden' => true, + ]); + + if ($this->saved_outputs->get('composer_json_exists') == 'exists') { + $this->execute_remote_command([ + executeInDocker($this->deployment_uuid, 'grep -E -q "laravel/framework|symfony/dotenv|symfony/framework-bundle|symfony/flex" '.$this->workdir.'/composer.json 2>/dev/null && echo "true" || echo "false"'), + 'save' => 'is_laravel_or_symfony', + 'hidden' => true, + ]); + + $this->is_laravel_or_symfony = $this->saved_outputs->get('is_laravel_or_symfony') == 'true'; + + if ($this->is_laravel_or_symfony) { + $this->application_deployment_queue->addLogEntry('Laravel/Symfony framework detected. Setting NIXPACKS PHP variables.'); + $this->ensure_nixpacks_php_variables(); + } + } + } + + private function ensure_nixpacks_php_variables() + { + if ($this->pull_request_id === 0) { + $envType = 'environment_variables'; + } else { + $envType = 'environment_variables_preview'; + } + + $nixpacks_php_fallback_path = $this->application->{$envType}->where('key', 'NIXPACKS_PHP_FALLBACK_PATH')->first(); + $nixpacks_php_root_dir = $this->application->{$envType}->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first(); + + $created_new = false; + if (! $nixpacks_php_fallback_path) { + $nixpacks_php_fallback_path = new EnvironmentVariable; + $nixpacks_php_fallback_path->key = 'NIXPACKS_PHP_FALLBACK_PATH'; + $nixpacks_php_fallback_path->value = '/index.php'; + $nixpacks_php_fallback_path->is_buildtime = true; + $nixpacks_php_fallback_path->is_preview = $this->pull_request_id !== 0; + $nixpacks_php_fallback_path->resourceable_id = $this->application->id; + $nixpacks_php_fallback_path->resourceable_type = 'App\Models\Application'; + $nixpacks_php_fallback_path->save(); + $this->application_deployment_queue->addLogEntry('Created NIXPACKS_PHP_FALLBACK_PATH environment variable.'); + $created_new = true; + } + if (! $nixpacks_php_root_dir) { + $nixpacks_php_root_dir = new EnvironmentVariable; + $nixpacks_php_root_dir->key = 'NIXPACKS_PHP_ROOT_DIR'; + $nixpacks_php_root_dir->value = '/app/public'; + $nixpacks_php_root_dir->is_buildtime = true; + $nixpacks_php_root_dir->is_preview = $this->pull_request_id !== 0; + $nixpacks_php_root_dir->resourceable_id = $this->application->id; + $nixpacks_php_root_dir->resourceable_type = 'App\Models\Application'; + $nixpacks_php_root_dir->save(); + $this->application_deployment_queue->addLogEntry('Created NIXPACKS_PHP_ROOT_DIR environment variable.'); + $created_new = true; + } + + // Always refresh the relationships to ensure we have the latest data + // This is critical for the first deployment where variables were just created + if ($this->pull_request_id === 0) { + $this->application->load(['nixpacks_environment_variables', 'environment_variables']); + } else { + $this->application->load(['nixpacks_environment_variables_preview', 'environment_variables_preview']); + } + + // // Export these variables to /etc/environment in the helper container + // $this->execute_remote_command([ + // executeInDocker($this->deployment_uuid, "echo 'NIXPACKS_PHP_FALLBACK_PATH=\"{$nixpacks_php_fallback_path->value}\"' >> /etc/environment"), + // 'hidden' => true, + // ], [ + // executeInDocker($this->deployment_uuid, "echo 'NIXPACKS_PHP_ROOT_DIR=\"{$nixpacks_php_root_dir->value}\"' >> /etc/environment"), + // 'hidden' => true, + // ]); + } + private function cleanup_git() { $this->execute_remote_command( @@ -1781,30 +1819,51 @@ private function cleanup_git() private function generate_nixpacks_confs() { - $nixpacks_command = $this->nixpacks_build_cmd(); - $this->application_deployment_queue->addLogEntry("Generating nixpacks configuration with: $nixpacks_command"); - $this->execute_remote_command( - [executeInDocker($this->deployment_uuid, $nixpacks_command), 'save' => 'nixpacks_plan', 'hidden' => true], [executeInDocker($this->deployment_uuid, "nixpacks detect {$this->workdir}"), 'save' => 'nixpacks_type', 'hidden' => true], ); + if ($this->saved_outputs->get('nixpacks_type')) { $this->nixpacks_type = $this->saved_outputs->get('nixpacks_type'); if (str($this->nixpacks_type)->isEmpty()) { throw new RuntimeException('Nixpacks failed to detect the application type. Please check the documentation of Nixpacks: https://nixpacks.com/docs/providers'); } } + $nixpacks_command = $this->nixpacks_build_cmd(); + $this->application_deployment_queue->addLogEntry("Generating nixpacks configuration with: $nixpacks_command"); + + $this->execute_remote_command( + [executeInDocker($this->deployment_uuid, $nixpacks_command), 'save' => 'nixpacks_plan', 'hidden' => true], + ); if ($this->saved_outputs->get('nixpacks_plan')) { $this->nixpacks_plan = $this->saved_outputs->get('nixpacks_plan'); if ($this->nixpacks_plan) { $this->application_deployment_queue->addLogEntry("Found application type: {$this->nixpacks_type}."); $this->application_deployment_queue->addLogEntry("If you need further customization, please check the documentation of Nixpacks: https://nixpacks.com/docs/providers/{$this->nixpacks_type}"); - $parsed = Toml::Parse($this->nixpacks_plan); + $parsed = json_decode($this->nixpacks_plan); // Do any modifications here // We need to generate envs here because nixpacks need to know to generate a proper Dockerfile $this->generate_env_variables(); + + if ($this->is_laravel_or_symfony) { + if ($this->pull_request_id === 0) { + $envType = 'environment_variables'; + } else { + $envType = 'environment_variables_preview'; + } + $nixpacks_php_fallback_path = $this->application->{$envType}->where('key', 'NIXPACKS_PHP_FALLBACK_PATH')->first(); + $nixpacks_php_root_dir = $this->application->{$envType}->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first(); + + if ($nixpacks_php_fallback_path) { + data_set($parsed, 'variables.NIXPACKS_PHP_FALLBACK_PATH', $nixpacks_php_fallback_path->value); + } + if ($nixpacks_php_root_dir) { + data_set($parsed, 'variables.NIXPACKS_PHP_ROOT_DIR', $nixpacks_php_root_dir->value); + } + } + $merged_envs = collect(data_get($parsed, 'variables', []))->merge($this->env_args); $aptPkgs = data_get($parsed, 'phases.setup.aptPkgs', []); if (count($aptPkgs) === 0) { @@ -1820,26 +1879,23 @@ private function generate_nixpacks_confs() data_set($parsed, 'phases.setup.aptPkgs', $aptPkgs); } data_set($parsed, 'variables', $merged_envs->toArray()); - $is_laravel = data_get($parsed, 'variables.IS_LARAVEL', false); - if ($is_laravel) { - $variables = $this->laravel_finetunes(); - data_set($parsed, 'variables.NIXPACKS_PHP_FALLBACK_PATH', $variables[0]->value); - data_set($parsed, 'variables.NIXPACKS_PHP_ROOT_DIR', $variables[1]->value); + + if ($this->is_laravel_or_symfony) { + $this->symfony_finetunes($parsed); } + if ($this->nixpacks_type === 'elixir') { $this->elixir_finetunes(); } - if ($this->nixpacks_type === 'php') { - $this->php_finetunes($parsed); - } - $this->nixpacks_plan = json_encode($parsed, JSON_PRETTY_PRINT); - $this->nixpacks_plan_json = collect($parsed); - $this->application_deployment_queue->addLogEntry("Final Nixpacks plan: {$this->nixpacks_plan}", hidden: true); + if ($this->nixpacks_type === 'rust') { // temporary: disable healthcheck for rust because the start phase does not have curl/wget $this->application->health_check_enabled = false; $this->application->save(); } + $this->nixpacks_plan = json_encode($parsed, JSON_PRETTY_PRINT); + $this->nixpacks_plan_json = collect($parsed); + $this->application_deployment_queue->addLogEntry("Final Nixpacks plan: {$this->nixpacks_plan}", hidden: true); } } } @@ -1847,7 +1903,7 @@ private function generate_nixpacks_confs() private function nixpacks_build_cmd() { $this->generate_nixpacks_env_variables(); - $nixpacks_command = "nixpacks plan -f toml {$this->env_nixpacks_args}"; + $nixpacks_command = "nixpacks plan -f json {$this->env_nixpacks_args}"; if ($this->application->build_command) { $nixpacks_command .= " --build-cmd \"{$this->application->build_command}\""; } @@ -2474,7 +2530,16 @@ private function build_image() } } + // if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) { + // $build_script = "#!/bin/bash\n"; + // $build_script .= "set -a\n"; + // $build_script .= "source /etc/environment 2>/dev/null || true\n"; + // $build_script .= "set +a\n"; + // $build_script .= $build_command; + // $base64_build_command = base64_encode($build_script); + // } else { $base64_build_command = base64_encode($build_command); + // } $this->execute_remote_command( [ executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), @@ -2540,7 +2605,16 @@ private function build_image() } } $build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"; + // if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) { + // $build_script = "#!/bin/bash\n"; + // $build_script .= "set -a\n"; + // $build_script .= "source /etc/environment 2>/dev/null || true\n"; + // $build_script .= "set +a\n"; + // $build_script .= $build_command; + // $base64_build_command = base64_encode($build_script); + // } else { $base64_build_command = base64_encode($build_command); + // } $this->execute_remote_command( [ executeInDocker($this->deployment_uuid, "echo '{$dockerfile}' | base64 -d | tee {$this->workdir}/Dockerfile > /dev/null"), @@ -2581,7 +2655,16 @@ private function build_image() $build_command = "docker build --pull {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"; } } + // if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) { + // $build_script = "#!/bin/bash\n"; + // $build_script .= "set -a\n"; + // $build_script .= "source /etc/environment 2>/dev/null || true\n"; + // $build_script .= "set +a\n"; + // $build_script .= $build_command; + // $base64_build_command = base64_encode($build_script); + // } else { $base64_build_command = base64_encode($build_command); + // } $this->execute_remote_command( [ executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), @@ -2633,7 +2716,17 @@ private function build_image() $build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile --progress plain -t {$this->production_image_name} {$this->build_args} {$this->workdir}"; } } + // If using build secrets, prepend source of /etc/environment to the build script + // if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) { + // $build_script = "#!/bin/bash\n"; + // $build_script .= "set -a\n"; + // $build_script .= "source /etc/environment 2>/dev/null || true\n"; + // $build_script .= "set +a\n"; + // $build_script .= $build_command; + // $base64_build_command = base64_encode($build_script); + // } else { $base64_build_command = base64_encode($build_command); + // } $this->execute_remote_command( [ executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), @@ -2667,7 +2760,17 @@ private function build_image() $build_command = "docker build {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"; } } + // If using build secrets, prepend source of /etc/environment to the build script + // if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) { + // $build_script = "#!/bin/bash\n"; + // $build_script .= "set -a\n"; + // $build_script .= "source /etc/environment 2>/dev/null || true\n"; + // $build_script .= "set +a\n"; + // $build_script .= $build_command; + // $base64_build_command = base64_encode($build_script); + // } else { $base64_build_command = base64_encode($build_command); + // } $this->execute_remote_command( [ executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), @@ -2985,7 +3088,6 @@ private function modify_dockerfile_for_secrets($dockerfile_path) $variables = $this->pull_request_id === 0 ? $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->where('is_buildtime', true)->get() : $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->where('is_buildtime', true)->get(); - if ($variables->isEmpty()) { return; }