diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index c429a8ae20..9287e2030d 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -2609,6 +2609,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $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->is_preview = $this->pull_request_id !== 0; $nixpacks_php_fallback_path->save(); } if (! $nixpacks_php_root_dir) { @@ -2617,6 +2618,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $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->is_preview = $this->pull_request_id !== 0; $nixpacks_php_root_dir->save(); } diff --git a/app/Models/Application.php b/app/Models/Application.php index 2b14b3c86b..fc9085752c 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -1168,7 +1168,7 @@ class Application extends BaseModel { return $this->morphMany(EnvironmentVariable::class, 'resourceable') ->where('is_preview', false) - ->withoutBuildpackControlVariables(); + ->withoutBuildOnlyBuildpackControlVariables(); } public function nixpacks_environment_variables() @@ -1203,7 +1203,7 @@ class Application extends BaseModel { return $this->morphMany(EnvironmentVariable::class, 'resourceable') ->where('is_preview', true) - ->withoutBuildpackControlVariables(); + ->withoutBuildOnlyBuildpackControlVariables(); } public function nixpacks_environment_variables_preview() diff --git a/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php index acb4ca139f..81ac70023c 100644 --- a/app/Models/EnvironmentVariable.php +++ b/app/Models/EnvironmentVariable.php @@ -44,6 +44,17 @@ class EnvironmentVariable extends BaseModel public const BUILDPACK_CONTROL_VARIABLE_PREFIXES = ['NIXPACKS_', 'RAILPACK_']; + /** + * Buildpack control variables that the built image reads when the container starts, + * e.g. the Nixpacks PHP prestart script renders nginx.conf from NIXPACKS_PHP_ROOT_DIR. + */ + public const RUNTIME_BUILDPACK_CONTROL_VARIABLES = [ + 'NIXPACKS_PHP_ROOT_DIR', + 'NIXPACKS_PHP_FALLBACK_PATH', + 'NIXPACKS_SPA_OUTPUT_DIR', + 'RAILPACK_SKIP_MIGRATIONS', + ]; + protected $attributes = [ 'is_runtime' => true, 'is_buildtime' => true, @@ -163,6 +174,14 @@ class EnvironmentVariable extends BaseModel return $query; } + public function scopeWithoutBuildOnlyBuildpackControlVariables(Builder $query): Builder + { + return $query->where(function (Builder $query) { + $query->whereIn('key', self::RUNTIME_BUILDPACK_CONTROL_VARIABLES) + ->orWhere(fn (Builder $query) => $query->withoutBuildpackControlVariables()); + }); + } + public static function isBuildpackControlKey(?string $key): bool { if (blank($key)) { diff --git a/tests/Feature/ApplicationDeploymentControlVarFilteringTest.php b/tests/Feature/ApplicationDeploymentControlVarFilteringTest.php index b3248f2d8d..f17a59e5df 100644 --- a/tests/Feature/ApplicationDeploymentControlVarFilteringTest.php +++ b/tests/Feature/ApplicationDeploymentControlVarFilteringTest.php @@ -943,6 +943,93 @@ it('filters buildpack control vars from preview runtime env fallback', function expect($runtimeEnvs->contains(fn (string $env) => str($env)->startsWith('RAILPACK_NODE_VERSION=')))->toBeFalse(); }); +it('keeps buildpack variables read at container start in the runtime env', function (int $pullRequestId, bool $isPreview) { + [$application, $server] = makeDeploymentControlVarFixture(['build_pack' => 'nixpacks']); + + foreach ([ + 'APP_NAME' => 'coolify', + 'NIXPACKS_PHP_ROOT_DIR' => '/app/public', + 'NIXPACKS_PHP_FALLBACK_PATH' => '/index.php', + 'NIXPACKS_SPA_OUTPUT_DIR' => 'dist', + 'RAILPACK_SKIP_MIGRATIONS' => 'true', + 'NIXPACKS_NODE_VERSION' => '22', + 'RAILPACK_NODE_VERSION' => '20', + ] as $key => $value) { + createApplicationEnvironmentVariable($application, [ + 'key' => $key, + 'value' => $value, + 'is_preview' => $isPreview, + ]); + } + + [$job, $reflection] = makeControlVarFilteringJob($application, $server, [ + 'pull_request_id' => $pullRequestId, + ]); + + /** @var Collection $runtimeEnvs */ + $runtimeEnvs = invokeDeploymentJobMethod($job, $reflection, 'generate_runtime_environment_variables'); + + expect($runtimeEnvs)->toContain( + 'APP_NAME='.escapeComposeEnvFileValue('coolify'), + 'NIXPACKS_PHP_ROOT_DIR='.escapeComposeEnvFileValue('/app/public'), + 'NIXPACKS_PHP_FALLBACK_PATH='.escapeComposeEnvFileValue('/index.php'), + 'NIXPACKS_SPA_OUTPUT_DIR='.escapeComposeEnvFileValue('dist'), + 'RAILPACK_SKIP_MIGRATIONS='.escapeComposeEnvFileValue('true'), + ); + expect($runtimeEnvs->contains(fn (string $env) => str($env)->startsWith('NIXPACKS_NODE_VERSION=')))->toBeFalse(); + expect($runtimeEnvs->contains(fn (string $env) => str($env)->startsWith('RAILPACK_NODE_VERSION=')))->toBeFalse(); +})->with([ + 'production' => [0, false], + 'preview' => [42, true], +]); + +it('keeps buildpack variables read at container start out of generic build args', function () { + [$application, $server] = makeDeploymentControlVarFixture(); + + createApplicationEnvironmentVariable($application, [ + 'key' => 'NIXPACKS_PHP_ROOT_DIR', + 'value' => '/app/public', + ]); + createApplicationEnvironmentVariable($application, [ + 'key' => 'RAILPACK_SKIP_MIGRATIONS', + 'value' => 'true', + ]); + + [$job, $reflection] = makeControlVarFilteringJob($application, $server); + + invokeDeploymentJobMethod($job, $reflection, 'generate_env_variables'); + + /** @var Collection $envArgs */ + $envArgs = readDeploymentJobProperty($job, $reflection, 'env_args'); + + expect($envArgs->has('NIXPACKS_PHP_ROOT_DIR'))->toBeFalse(); + expect($envArgs->has('RAILPACK_SKIP_MIGRATIONS'))->toBeFalse(); +}); + +it('creates missing Laravel defaults as preview variables for preview deployments', function () { + [$application, $server] = makeDeploymentControlVarFixture(['build_pack' => 'nixpacks']); + + $keys = ['NIXPACKS_PHP_ROOT_DIR', 'NIXPACKS_PHP_FALLBACK_PATH']; + foreach (array_combine($keys, ['/app/web', '/app.php']) as $key => $value) { + createApplicationEnvironmentVariable($application, ['key' => $key, 'value' => $value]); + } + $application->environment_variables_preview()->whereIn('key', $keys)->delete(); + + [$job, $reflection] = makeControlVarFilteringJob($application, $server, ['pull_request_id' => 42]); + + [$fallbackPath, $rootDir] = invokeDeploymentJobMethod($job, $reflection, 'laravel_finetunes'); + + expect($fallbackPath->is_preview)->toBeTrue() + ->and($fallbackPath->value)->toBe('/index.php') + ->and($rootDir->is_preview)->toBeTrue() + ->and($rootDir->value)->toBe('/app/public') + ->and($application->environment_variables()->whereIn('key', $keys)->pluck('value', 'key')->all())->toBe([ + 'NIXPACKS_PHP_ROOT_DIR' => '/app/web', + 'NIXPACKS_PHP_FALLBACK_PATH' => '/app.php', + ]) + ->and($application->environment_variables_preview()->whereIn('key', $keys)->count())->toBe(2); +}); + it('filters buildpack control vars from dockerfile arg injection', function () { [$application, $server] = makeDeploymentControlVarFixture();