Fix Nixpacks null environment variable parsing error

Filter out null and empty environment variables when generating Nixpacks build
configuration to prevent JSON parsing errors. Environment variables with null or
empty values were being passed as `--env KEY=` which created invalid JSON with
null values, causing deployment failures.

This fix ensures only valid non-empty environment variables are included in both
user-defined and auto-generated COOLIFY_* environment variables.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai 2025-12-04 15:10:39 +01:00
parent 05eed974cb
commit 42f08a99fb
2 changed files with 305 additions and 3 deletions

View file

@ -2281,13 +2281,13 @@ private function generate_nixpacks_env_variables()
$this->env_nixpacks_args = collect([]);
if ($this->pull_request_id === 0) {
foreach ($this->application->nixpacks_environment_variables as $env) {
if (! is_null($env->real_value)) {
if (! is_null($env->real_value) && $env->real_value !== '') {
$this->env_nixpacks_args->push("--env {$env->key}={$env->real_value}");
}
}
} else {
foreach ($this->application->nixpacks_environment_variables_preview as $env) {
if (! is_null($env->real_value)) {
if (! is_null($env->real_value) && $env->real_value !== '') {
$this->env_nixpacks_args->push("--env {$env->key}={$env->real_value}");
}
}
@ -2296,7 +2296,10 @@ private function generate_nixpacks_env_variables()
// Add COOLIFY_* environment variables to Nixpacks build context
$coolify_envs = $this->generate_coolify_env_variables(forBuildTime: true);
$coolify_envs->each(function ($value, $key) {
$this->env_nixpacks_args->push("--env {$key}={$value}");
// Only add environment variables with non-null and non-empty values
if (! is_null($value) && $value !== '') {
$this->env_nixpacks_args->push("--env {$key}={$value}");
}
});
$this->env_nixpacks_args = $this->env_nixpacks_args->implode(' ');

View file

@ -0,0 +1,299 @@
<?php
use App\Jobs\ApplicationDeploymentJob;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Models\EnvironmentVariable;
/**
* Test to verify that null and empty environment variables are filtered out
* when generating Nixpacks configuration.
*
* This test verifies the fix for the issue where null or empty environment variable
* values would be passed to Nixpacks as `--env KEY=` (with no value), causing
* JSON parsing errors: "invalid type: null, expected a string at line 12 column 27"
*
* The fix ensures that:
* 1. User-defined environment variables with null or empty values are filtered out
* 2. COOLIFY_* environment variables with null or empty values are filtered out
* 3. Only environment variables with valid non-empty values are passed to Nixpacks
*/
it('filters out null environment variables from nixpacks build command', function () {
// Mock application with nixpacks build pack
$mockApplication = Mockery::mock(Application::class);
$mockApplication->shouldReceive('getAttribute')
->with('build_pack')
->andReturn('nixpacks');
$mockApplication->build_pack = 'nixpacks';
// Mock environment variables - some with null/empty values
$envVar1 = Mockery::mock(EnvironmentVariable::class);
$envVar1->key = 'VALID_VAR';
$envVar1->real_value = 'valid_value';
$envVar2 = Mockery::mock(EnvironmentVariable::class);
$envVar2->key = 'NULL_VAR';
$envVar2->real_value = null;
$envVar3 = Mockery::mock(EnvironmentVariable::class);
$envVar3->key = 'EMPTY_VAR';
$envVar3->real_value = '';
$envVar4 = Mockery::mock(EnvironmentVariable::class);
$envVar4->key = 'ANOTHER_VALID_VAR';
$envVar4->real_value = 'another_value';
$nixpacksEnvVars = collect([$envVar1, $envVar2, $envVar3, $envVar4]);
$mockApplication->shouldReceive('getAttribute')
->with('nixpacks_environment_variables')
->andReturn($nixpacksEnvVars);
// Mock application deployment queue
$mockQueue = Mockery::mock(ApplicationDeploymentQueue::class);
$mockQueue->shouldReceive('getAttribute')->with('application_id')->andReturn(1);
$mockQueue->application_id = 1;
// Mock the job
$job = Mockery::mock(ApplicationDeploymentJob::class)->makePartial();
$job->shouldAllowMockingProtectedMethods();
$reflection = new \ReflectionClass(ApplicationDeploymentJob::class);
// Set private properties
$applicationProperty = $reflection->getProperty('application');
$applicationProperty->setAccessible(true);
$applicationProperty->setValue($job, $mockApplication);
$pullRequestProperty = $reflection->getProperty('pull_request_id');
$pullRequestProperty->setAccessible(true);
$pullRequestProperty->setValue($job, 0);
// Mock generate_coolify_env_variables to return some values including null
$job->shouldReceive('generate_coolify_env_variables')
->andReturn(collect([
'COOLIFY_FQDN' => 'example.com',
'COOLIFY_URL' => null, // null value that should be filtered
'COOLIFY_BRANCH' => '', // empty value that should be filtered
'SOURCE_COMMIT' => 'abc123',
]));
// Call the private method
$method = $reflection->getMethod('generate_nixpacks_env_variables');
$method->setAccessible(true);
$method->invoke($job);
// Get the generated env_nixpacks_args
$envArgsProperty = $reflection->getProperty('env_nixpacks_args');
$envArgsProperty->setAccessible(true);
$envArgs = $envArgsProperty->getValue($job);
// Verify that only valid environment variables are included
expect($envArgs)->toContain('--env VALID_VAR=valid_value');
expect($envArgs)->toContain('--env ANOTHER_VALID_VAR=another_value');
expect($envArgs)->toContain('--env COOLIFY_FQDN=example.com');
expect($envArgs)->toContain('--env SOURCE_COMMIT=abc123');
// Verify that null and empty environment variables are filtered out
expect($envArgs)->not->toContain('NULL_VAR');
expect($envArgs)->not->toContain('EMPTY_VAR');
expect($envArgs)->not->toContain('COOLIFY_URL');
expect($envArgs)->not->toContain('COOLIFY_BRANCH');
// Verify no environment variables end with just '=' (which indicates null/empty value)
expect($envArgs)->not->toMatch('/--env [A-Z_]+=$/');
expect($envArgs)->not->toMatch('/--env [A-Z_]+= /');
});
it('filters out null environment variables from nixpacks preview deployments', function () {
// Mock application with nixpacks build pack
$mockApplication = Mockery::mock(Application::class);
$mockApplication->shouldReceive('getAttribute')
->with('build_pack')
->andReturn('nixpacks');
$mockApplication->build_pack = 'nixpacks';
// Mock preview environment variables - some with null/empty values
$envVar1 = Mockery::mock(EnvironmentVariable::class);
$envVar1->key = 'PREVIEW_VAR';
$envVar1->real_value = 'preview_value';
$envVar2 = Mockery::mock(EnvironmentVariable::class);
$envVar2->key = 'NULL_PREVIEW_VAR';
$envVar2->real_value = null;
$previewEnvVars = collect([$envVar1, $envVar2]);
$mockApplication->shouldReceive('getAttribute')
->with('nixpacks_environment_variables_preview')
->andReturn($previewEnvVars);
// Mock application deployment queue
$mockQueue = Mockery::mock(ApplicationDeploymentQueue::class);
$mockQueue->shouldReceive('getAttribute')->with('application_id')->andReturn(1);
$mockQueue->application_id = 1;
// Mock the job
$job = Mockery::mock(ApplicationDeploymentJob::class)->makePartial();
$job->shouldAllowMockingProtectedMethods();
$reflection = new \ReflectionClass(ApplicationDeploymentJob::class);
// Set private properties
$applicationProperty = $reflection->getProperty('application');
$applicationProperty->setAccessible(true);
$applicationProperty->setValue($job, $mockApplication);
$pullRequestProperty = $reflection->getProperty('pull_request_id');
$pullRequestProperty->setAccessible(true);
$pullRequestProperty->setValue($job, 123); // Non-zero for preview deployment
// Mock generate_coolify_env_variables
$job->shouldReceive('generate_coolify_env_variables')
->andReturn(collect([
'COOLIFY_FQDN' => 'preview.example.com',
]));
// Call the private method
$method = $reflection->getMethod('generate_nixpacks_env_variables');
$method->setAccessible(true);
$method->invoke($job);
// Get the generated env_nixpacks_args
$envArgsProperty = $reflection->getProperty('env_nixpacks_args');
$envArgsProperty->setAccessible(true);
$envArgs = $envArgsProperty->getValue($job);
// Verify that only valid environment variables are included
expect($envArgs)->toContain('--env PREVIEW_VAR=preview_value');
expect($envArgs)->toContain('--env COOLIFY_FQDN=preview.example.com');
// Verify that null environment variables are filtered out
expect($envArgs)->not->toContain('NULL_PREVIEW_VAR');
});
it('handles all environment variables being null or empty', function () {
// Mock application with nixpacks build pack
$mockApplication = Mockery::mock(Application::class);
$mockApplication->shouldReceive('getAttribute')
->with('build_pack')
->andReturn('nixpacks');
$mockApplication->build_pack = 'nixpacks';
// Mock environment variables - all null or empty
$envVar1 = Mockery::mock(EnvironmentVariable::class);
$envVar1->key = 'NULL_VAR';
$envVar1->real_value = null;
$envVar2 = Mockery::mock(EnvironmentVariable::class);
$envVar2->key = 'EMPTY_VAR';
$envVar2->real_value = '';
$nixpacksEnvVars = collect([$envVar1, $envVar2]);
$mockApplication->shouldReceive('getAttribute')
->with('nixpacks_environment_variables')
->andReturn($nixpacksEnvVars);
// Mock application deployment queue
$mockQueue = Mockery::mock(ApplicationDeploymentQueue::class);
$mockQueue->shouldReceive('getAttribute')->with('application_id')->andReturn(1);
$mockQueue->application_id = 1;
// Mock the job
$job = Mockery::mock(ApplicationDeploymentJob::class)->makePartial();
$job->shouldAllowMockingProtectedMethods();
$reflection = new \ReflectionClass(ApplicationDeploymentJob::class);
// Set private properties
$applicationProperty = $reflection->getProperty('application');
$applicationProperty->setAccessible(true);
$applicationProperty->setValue($job, $mockApplication);
$pullRequestProperty = $reflection->getProperty('pull_request_id');
$pullRequestProperty->setAccessible(true);
$pullRequestProperty->setValue($job, 0);
// Mock generate_coolify_env_variables to return all null/empty values
$job->shouldReceive('generate_coolify_env_variables')
->andReturn(collect([
'COOLIFY_URL' => null,
'COOLIFY_BRANCH' => '',
]));
// Call the private method
$method = $reflection->getMethod('generate_nixpacks_env_variables');
$method->setAccessible(true);
$method->invoke($job);
// Get the generated env_nixpacks_args
$envArgsProperty = $reflection->getProperty('env_nixpacks_args');
$envArgsProperty->setAccessible(true);
$envArgs = $envArgsProperty->getValue($job);
// Verify that the result is empty or contains no environment variables
expect($envArgs)->toBe('');
});
it('preserves environment variables with zero values', function () {
// Mock application with nixpacks build pack
$mockApplication = Mockery::mock(Application::class);
$mockApplication->shouldReceive('getAttribute')
->with('build_pack')
->andReturn('nixpacks');
$mockApplication->build_pack = 'nixpacks';
// Mock environment variables with zero values (which should NOT be filtered)
$envVar1 = Mockery::mock(EnvironmentVariable::class);
$envVar1->key = 'ZERO_VALUE';
$envVar1->real_value = '0';
$envVar2 = Mockery::mock(EnvironmentVariable::class);
$envVar2->key = 'FALSE_VALUE';
$envVar2->real_value = 'false';
$nixpacksEnvVars = collect([$envVar1, $envVar2]);
$mockApplication->shouldReceive('getAttribute')
->with('nixpacks_environment_variables')
->andReturn($nixpacksEnvVars);
// Mock application deployment queue
$mockQueue = Mockery::mock(ApplicationDeploymentQueue::class);
$mockQueue->shouldReceive('getAttribute')->with('application_id')->andReturn(1);
$mockQueue->application_id = 1;
// Mock the job
$job = Mockery::mock(ApplicationDeploymentJob::class)->makePartial();
$job->shouldAllowMockingProtectedMethods();
$reflection = new \ReflectionClass(ApplicationDeploymentJob::class);
// Set private properties
$applicationProperty = $reflection->getProperty('application');
$applicationProperty->setAccessible(true);
$applicationProperty->setValue($job, $mockApplication);
$pullRequestProperty = $reflection->getProperty('pull_request_id');
$pullRequestProperty->setAccessible(true);
$pullRequestProperty->setValue($job, 0);
// Mock generate_coolify_env_variables
$job->shouldReceive('generate_coolify_env_variables')
->andReturn(collect([]));
// Call the private method
$method = $reflection->getMethod('generate_nixpacks_env_variables');
$method->setAccessible(true);
$method->invoke($job);
// Get the generated env_nixpacks_args
$envArgsProperty = $reflection->getProperty('env_nixpacks_args');
$envArgsProperty->setAccessible(true);
$envArgs = $envArgsProperty->getValue($job);
// Verify that zero and false string values are preserved
expect($envArgs)->toContain('--env ZERO_VALUE=0');
expect($envArgs)->toContain('--env FALSE_VALUE=false');
});