feat/fix(deployment): implement detection for Laravel/Symfony frameworks and configure NIXPACKS PHP environment variables accordingly

This commit is contained in:
Andras Bacsai 2025-09-29 12:05:14 +02:00
parent cd2d4070d3
commit ed7ecbb49d

View file

@ -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;
}