coolify/tests/Feature/ApplicationDeploymentControlVarFilteringTest.php
Andras Bacsai b3339d1034 feat(railpack): add buildpack control var filtering and dev seeder
Extract NIXPACKS_/RAILPACK_ prefix filtering into a reusable
`scopeWithoutBuildpackControlVariables` query scope on EnvironmentVariable.
Apply scope consistently to runtime vars, runtime preview vars, and
buildtime var generation in ApplicationDeploymentJob.

Refactor `generate_railpack_env_variables` to return a Collection.
Add `RAILPACK_FRONTEND_IMAGE` constant and bake it into the
coolify-helper Dockerfile as a build arg.

Add DevelopmentRailpackExamplesSeeder (dev/local env only) for
seeding example Railpack apps, wired into DatabaseSeeder.

Add tests:
- ApplicationDeploymentControlVarFilteringTest: verifies control vars
  are excluded from runtime and buildtime envs
- DevelopmentRailpackExamplesSeederTest: verifies seeder behavior
- ApplicationDeploymentRailpackEnvParityTest: parity checks for env
  handling across build/runtime paths
2026-04-28 14:37:31 +02:00

287 lines
10 KiB
PHP

<?php
use App\Jobs\ApplicationDeploymentJob;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Models\ApplicationPreview;
use App\Models\Environment;
use App\Models\EnvironmentVariable;
use App\Models\Project;
use App\Models\Server;
use App\Models\Team;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Collection;
uses(RefreshDatabase::class);
class TestableControlVarFilteringDeploymentJob extends ApplicationDeploymentJob
{
public array $recordedCommands = [];
public ?string $writtenDockerfile = null;
public function __construct() {}
public function execute_remote_command(...$commands)
{
$this->recordedCommands[] = $commands;
foreach ($commands as $command) {
$commandString = is_array($command) ? ($command['command'] ?? $command[0] ?? null) : $command;
if (! is_string($commandString)) {
continue;
}
if (preg_match('/echo .*?([A-Za-z0-9+\\/=]{16,}).*?\\| base64 -d \\| tee \\/artifacts\\/test-app\\/Dockerfile > \\/dev\\/null/', $commandString, $matches) === 1) {
$this->writtenDockerfile = base64_decode($matches[1]) ?: null;
}
}
}
}
function makeDeploymentControlVarFixture(array $applicationAttributes = []): array
{
$team = Team::create([
'name' => 'Control Var Team',
'description' => 'Team for deployment control var tests.',
'personal_team' => false,
'show_boarding' => false,
]);
$project = Project::create([
'name' => 'Control Var Project',
'team_id' => $team->id,
]);
$environment = Environment::where('project_id', $project->id)->firstOrFail();
$server = Server::factory()->create([
'team_id' => $team->id,
]);
$application = Application::factory()->create([
'environment_id' => $environment->id,
'build_pack' => 'dockerfile',
...$applicationAttributes,
]);
$application->settings()->update([
'inject_build_args_to_dockerfile' => true,
'include_source_commit_in_build' => false,
'is_env_sorting_enabled' => false,
]);
return [$application->fresh(), $server];
}
function createApplicationEnvironmentVariable(Application $application, array $attributes): EnvironmentVariable
{
return EnvironmentVariable::create([
'resourceable_type' => Application::class,
'resourceable_id' => $application->id,
'is_preview' => false,
'is_runtime' => true,
'is_buildtime' => true,
'is_multiline' => false,
'is_literal' => false,
...$attributes,
]);
}
function makeControlVarFilteringJob(Application $application, Server $server, array $overrides = []): array
{
$job = new TestableControlVarFilteringDeploymentJob;
$reflection = new ReflectionClass(ApplicationDeploymentJob::class);
$queue = Mockery::mock(ApplicationDeploymentQueue::class);
$queue->shouldReceive('addLogEntry')->andReturnNull();
$properties = [
'application' => $application->fresh(),
'application_deployment_queue' => $queue,
'build_pack' => $application->build_pack,
'mainServer' => $server,
'pull_request_id' => 0,
'commit' => 'HEAD',
'workdir' => '/artifacts/test-app',
'deployment_uuid' => 'deployment-uuid',
'dockerfile_location' => '/Dockerfile',
'container_name' => 'control-var-app',
'coolify_variables' => null,
'dockerSecretsSupported' => false,
];
$mergedProperties = array_merge($properties, $overrides);
$mergedProperties['saved_outputs'] = new Collection($overrides['saved_outputs'] ?? []);
if (($mergedProperties['pull_request_id'] ?? 0) !== 0 && ! array_key_exists('preview', $mergedProperties)) {
$mergedProperties['preview'] = ApplicationPreview::create([
'application_id' => $application->id,
'pull_request_id' => $mergedProperties['pull_request_id'],
'pull_request_html_url' => 'https://example.com/pr/'.$mergedProperties['pull_request_id'],
'fqdn' => 'https://preview.example.com',
]);
}
foreach ($mergedProperties as $property => $value) {
$reflectionProperty = $reflection->getProperty($property);
$reflectionProperty->setAccessible(true);
$reflectionProperty->setValue($job, $value);
}
return [$job, $reflection];
}
function invokeDeploymentJobMethod(object $job, ReflectionClass $reflection, string $method): mixed
{
$reflectionMethod = $reflection->getMethod($method);
$reflectionMethod->setAccessible(true);
return $reflectionMethod->invoke($job);
}
function readDeploymentJobProperty(object $job, ReflectionClass $reflection, string $property): mixed
{
$reflectionProperty = $reflection->getProperty($property);
$reflectionProperty->setAccessible(true);
return $reflectionProperty->getValue($job);
}
it('filters buildpack control vars from generic build args', function () {
[$application, $server] = makeDeploymentControlVarFixture();
createApplicationEnvironmentVariable($application, [
'key' => 'APP_ENV',
'value' => 'production',
]);
createApplicationEnvironmentVariable($application, [
'key' => 'NIXPACKS_NODE_VERSION',
'value' => '22',
]);
createApplicationEnvironmentVariable($application, [
'key' => 'RAILPACK_NODE_VERSION',
'value' => '20',
]);
[$job, $reflection] = makeControlVarFilteringJob($application, $server);
invokeDeploymentJobMethod($job, $reflection, 'generate_env_variables');
/** @var Collection $envArgs */
$envArgs = readDeploymentJobProperty($job, $reflection, 'env_args');
expect($envArgs->get('APP_ENV'))->toBe('production');
expect($envArgs->has('NIXPACKS_NODE_VERSION'))->toBeFalse();
expect($envArgs->has('RAILPACK_NODE_VERSION'))->toBeFalse();
});
it('filters buildpack control vars from preview build-time env files', function () {
[$application, $server] = makeDeploymentControlVarFixture();
createApplicationEnvironmentVariable($application, [
'key' => 'APP_ENV',
'value' => 'production',
'is_preview' => true,
]);
createApplicationEnvironmentVariable($application, [
'key' => 'NIXPACKS_NODE_VERSION',
'value' => '22',
'is_preview' => true,
]);
createApplicationEnvironmentVariable($application, [
'key' => 'RAILPACK_NODE_VERSION',
'value' => '20',
'is_preview' => true,
]);
[$job, $reflection] = makeControlVarFilteringJob($application, $server, [
'pull_request_id' => 42,
]);
/** @var Collection $buildtimeEnvs */
$buildtimeEnvs = invokeDeploymentJobMethod($job, $reflection, 'generate_buildtime_environment_variables');
expect($buildtimeEnvs->contains(fn (string $env) => str($env)->startsWith('APP_ENV=')))->toBeTrue();
expect($buildtimeEnvs->contains(fn (string $env) => str($env)->startsWith('NIXPACKS_NODE_VERSION=')))->toBeFalse();
expect($buildtimeEnvs->contains(fn (string $env) => str($env)->startsWith('RAILPACK_NODE_VERSION=')))->toBeFalse();
});
it('filters buildpack control vars from preview runtime env fallback', function () {
[$application, $server] = makeDeploymentControlVarFixture();
createApplicationEnvironmentVariable($application, [
'key' => 'APP_NAME',
'value' => 'coolify',
'is_runtime' => true,
'is_buildtime' => false,
]);
createApplicationEnvironmentVariable($application, [
'key' => 'NIXPACKS_NODE_VERSION',
'value' => '22',
'is_runtime' => true,
'is_buildtime' => false,
]);
createApplicationEnvironmentVariable($application, [
'key' => 'RAILPACK_NODE_VERSION',
'value' => '20',
'is_runtime' => true,
'is_buildtime' => false,
]);
createApplicationEnvironmentVariable($application, [
'key' => 'PREVIEW_FLAG',
'value' => 'enabled',
'is_preview' => true,
'is_runtime' => true,
'is_buildtime' => false,
]);
$application->environment_variables_preview()
->whereIn('key', ['APP_NAME', 'NIXPACKS_NODE_VERSION', 'RAILPACK_NODE_VERSION'])
->delete();
[$job, $reflection] = makeControlVarFilteringJob($application, $server, [
'pull_request_id' => 99,
]);
/** @var Collection $runtimeEnvs */
$runtimeEnvs = invokeDeploymentJobMethod($job, $reflection, 'generate_runtime_environment_variables');
expect($runtimeEnvs->contains(fn (string $env) => str($env)->startsWith('APP_NAME=')))->toBeTrue();
expect($runtimeEnvs->contains(fn (string $env) => str($env)->startsWith('PREVIEW_FLAG=')))->toBeTrue();
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();
});
it('filters buildpack control vars from dockerfile arg injection', function () {
[$application, $server] = makeDeploymentControlVarFixture();
createApplicationEnvironmentVariable($application, [
'key' => 'APP_ENV',
'value' => 'production',
'is_runtime' => false,
'is_buildtime' => true,
]);
createApplicationEnvironmentVariable($application, [
'key' => 'NIXPACKS_NODE_VERSION',
'value' => '22',
'is_runtime' => false,
'is_buildtime' => true,
]);
createApplicationEnvironmentVariable($application, [
'key' => 'RAILPACK_NODE_VERSION',
'value' => '20',
'is_runtime' => false,
'is_buildtime' => true,
]);
[$job, $reflection] = makeControlVarFilteringJob($application, $server, [
'saved_outputs' => [
'dockerfile' => "FROM php:8.4-cli\nRUN php -v",
],
]);
invokeDeploymentJobMethod($job, $reflection, 'add_build_env_variables_to_dockerfile');
expect($job->writtenDockerfile)->toContain('ARG APP_ENV=production');
expect($job->writtenDockerfile)->not->toContain('ARG NIXPACKS_NODE_VERSION=');
expect($job->writtenDockerfile)->not->toContain('ARG RAILPACK_NODE_VERSION=');
});