fix(railpack): interpolate build-time env variables by sourcing build… (#10768)

This commit is contained in:
Andras Bacsai 2026-06-29 11:08:19 +02:00 committed by GitHub
commit f2d11d9300
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 92 additions and 6 deletions

View file

@ -1833,8 +1833,8 @@ private function save_buildtime_environment_variables()
]
);
}
} elseif ($this->build_pack === 'dockercompose' || $this->build_pack === 'dockerfile') {
// For Docker Compose and Dockerfile, create an empty .env file even if there are no build-time variables
} elseif (in_array($this->build_pack, ['dockercompose', 'dockerfile', 'railpack'], true)) {
// For build packs that source the build-time .env file, create an empty file even if there are no build-time variables
// This ensures the file exists when referenced in build commands
$this->application_deployment_queue->addLogEntry('Creating empty build-time .env file in /artifacts (no build-time variables defined).', hidden: true);
@ -2697,12 +2697,22 @@ private function railpack_build_command(string $imageName, Collection $variables
$cacheArgs .= ' --build-arg secrets-hash='.$this->generate_secrets_hash($variables);
}
$environmentPrefix = $this->railpack_build_environment_prefix($variables);
// Build-time variables reach the build through the sourced build-time .env file
// (written by save_buildtime_environment_variables), which interpolates shell-style
// references such as BETTER_AUTH_URL=$COOLIFY_URL. Passing them inline via `env`
// would forward the literal `$COOLIFY_URL` because each value is single-quoted and
// `env` does not interpolate its own assignments. Only buildpack control variables
// (NIXPACKS_/RAILPACK_) — which are excluded from the build-time .env file and never
// need interpolation — are still passed inline.
$controlVariables = $variables->filter(
fn ($value, $key) => str($key)->startsWith(EnvironmentVariable::BUILDPACK_CONTROL_VARIABLE_PREFIXES)
);
$environmentPrefix = $this->railpack_build_environment_prefix($controlVariables);
$secretFlags = $this->railpack_build_secret_flags($variables);
$frontendImage = 'ghcr.io/railwayapp/railpack-frontend:v'.config('constants.coolify.railpack_version');
return 'docker buildx create --name coolify-railpack --driver docker-container 2>/dev/null || true'
." && {$environmentPrefix}docker buildx build --builder coolify-railpack"
$buildxBuildCommand = "{$environmentPrefix}docker buildx build --builder coolify-railpack"
." {$this->addHosts} --network host"
." --build-arg BUILDKIT_SYNTAX=\"{$frontendImage}\""
." {$cacheArgs}"
@ -2712,6 +2722,9 @@ private function railpack_build_command(string $imageName, Collection $variables
.' --load'
." -t {$imageName}"
." {$this->workdir}";
return 'docker buildx create --name coolify-railpack --driver docker-container 2>/dev/null || true'
.' && '.$this->wrap_build_command_with_env_export($buildxBuildCommand);
}
private function decode_railpack_config(string $config, string $source): array

View file

@ -3,6 +3,8 @@
use App\Exceptions\DeploymentException;
use App\Jobs\ApplicationDeploymentJob;
use App\Models\Application;
use App\Models\ApplicationSetting;
use App\Models\EnvironmentVariable;
use Illuminate\Support\Collection;
use Tests\TestCase;
@ -236,10 +238,15 @@ function invokeRailpackMethod(object $job, ReflectionClass $reflection, string $
],
);
// Build-time variables are interpolated by sourcing the build-time .env file before
// the build, so user/Coolify variables must NOT be forwarded inline as literals.
expect($command)->toContain('set -a && source /artifacts/build-time.env && set +a');
expect($command)->toContain("env 'RAILPACK_NODE_VERSION=22'");
expect($command)->toContain("'RAILPACK_INSTALL_CMD=npm ci && npm run postinstall'");
expect($command)->toContain("'RAILPACK_DEPLOY_APT_PACKAGES=curl wget'");
expect($command)->toContain("'SECRET_JSON={\"token\":\"abc\"}'");
// SECRET_JSON is not a buildpack control variable, so it is provided via the sourced
// build-time .env file (which supports $VAR interpolation) rather than inline `env`.
expect($command)->not->toContain("'SECRET_JSON={\"token\":\"abc\"}'");
expect($command)->toContain("--secret 'id=RAILPACK_NODE_VERSION,env=RAILPACK_NODE_VERSION'");
expect($command)->toContain("--secret 'id=RAILPACK_INSTALL_CMD,env=RAILPACK_INSTALL_CMD'");
expect($command)->toContain("--secret 'id=RAILPACK_DEPLOY_APT_PACKAGES,env=RAILPACK_DEPLOY_APT_PACKAGES'");
@ -247,3 +254,69 @@ function invokeRailpackMethod(object $job, ReflectionClass $reflection, string $
expect($command)->toContain(' --build-arg secrets-hash=');
expect($command)->toContain('--build-arg BUILDKIT_SYNTAX="ghcr.io/railwayapp/railpack-frontend:v'.config('constants.coolify.railpack_version').'"');
});
it('interpolates build-time variable references for railpack by sourcing the build-time env file', function () {
[$job, $reflection] = makeRailpackDeploymentJob([
'uuid' => 'application-uuid',
]);
// Mirrors the issue: BETTER_AUTH_URL=$COOLIFY_URL must be interpolated at build time.
$command = invokeRailpackMethod(
$job,
$reflection,
'railpack_build_command',
[
'coollabsio/coolify:test',
collect([
'BETTER_AUTH_URL' => '$COOLIFY_URL',
'COOLIFY_URL' => 'https://sapere-10.bobman.dev',
]),
],
);
// The literal `$COOLIFY_URL` must NOT be forwarded inline; it is resolved by the shell
// after sourcing the build-time .env file, then read through the build secret.
expect($command)->toContain('set -a && source /artifacts/build-time.env && set +a');
expect($command)->not->toContain("'BETTER_AUTH_URL=\$COOLIFY_URL'");
expect($command)->not->toContain("env 'BETTER_AUTH_URL");
expect($command)->toContain("--secret 'id=BETTER_AUTH_URL,env=BETTER_AUTH_URL'");
expect($command)->toContain("--secret 'id=COOLIFY_URL,env=COOLIFY_URL'");
});
it('creates an empty build-time env file for railpack when there are no generated build-time variables', function () {
[$job, $reflection] = makeRailpackDeploymentJob([
'build_pack' => 'railpack',
'compose_parsing_version' => '3',
]);
$applicationProperty = $reflection->getProperty('application');
$applicationProperty->setAccessible(true);
$application = $applicationProperty->getValue($job);
$application->setRelation('settings', new ApplicationSetting([
'include_source_commit_in_build' => false,
'is_env_sorting_enabled' => false,
]));
$application->setRelation('environment_variables', collect([
new EnvironmentVariable(['key' => 'COOLIFY_FQDN']),
new EnvironmentVariable(['key' => 'COOLIFY_URL']),
new EnvironmentVariable(['key' => 'COOLIFY_BRANCH']),
new EnvironmentVariable(['key' => 'COOLIFY_RESOURCE_UUID']),
]));
foreach ([
'application_deployment_queue' => new class
{
public function addLogEntry(string $message, string $type = 'info', bool $hidden = false): void {}
},
'build_pack' => 'railpack',
'pull_request_id' => 0,
] as $property => $value) {
$reflectionProperty = $reflection->getProperty($property);
$reflectionProperty->setAccessible(true);
$reflectionProperty->setValue($job, $value);
}
invokeRailpackMethod($job, $reflection, 'save_buildtime_environment_variables');
expect(collect($job->recordedCommands)->flatten()->implode(' '))->toContain('touch /artifacts/build-time.env');
});