fix(previews): deploy previews with their own environment variables (#12325)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai
2026-10-26 09:55:44 +01:00
committed by GitHub
co-authored by Claude Opus 5.5
parent ddeb05280e
commit 3f3fa1d758
3 changed files with 144 additions and 86 deletions
@@ -897,7 +897,7 @@ it('filters docker compose generated service variables from build args', functio
expect($envArgs->has('SERVICE_URL_APP'))->toBeFalse();
});
it('filters buildpack control vars from preview runtime env fallback', function () {
it('does not fall back to production vars when preview copies are deleted', function () {
[$application, $server] = makeDeploymentControlVarFixture();
createApplicationEnvironmentVariable($application, [
@@ -906,18 +906,6 @@ it('filters buildpack control vars from preview runtime env fallback', function
'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',
@@ -926,9 +914,7 @@ it('filters buildpack control vars from preview runtime env fallback', function
'is_buildtime' => false,
]);
$application->environment_variables_preview()
->whereIn('key', ['APP_NAME', 'NIXPACKS_NODE_VERSION', 'RAILPACK_NODE_VERSION'])
->delete();
$application->environment_variables_preview()->where('key', 'APP_NAME')->delete();
[$job, $reflection] = makeControlVarFilteringJob($application, $server, [
'pull_request_id' => 99,
@@ -937,10 +923,95 @@ it('filters buildpack control vars from preview runtime env fallback', function
/** @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('APP_NAME=')))->toBeFalse();
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('does not give production runtime values to a preview copy that is not a runtime variable', function () {
[$application, $server] = makeDeploymentControlVarFixture([
'build_pack' => 'dockercompose',
'docker_compose_raw' => "services:\n app:\n image: nginx\n",
]);
createApplicationEnvironmentVariable($application, [
'key' => 'DB_PASSWORD',
'value' => 'production-secret',
'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()->where('key', 'DB_PASSWORD')->update(['is_runtime' => false]);
[$job, $reflection] = makeControlVarFilteringJob($application, $server, [
'pull_request_id' => 99,
]);
/** @var Collection $runtimeEnvs */
$runtimeEnvs = invokeDeploymentJobMethod($job, $reflection, 'generate_runtime_environment_variables');
expect($runtimeEnvs->filter(fn (string $env) => str($env)->contains('production-secret')))->toBeEmpty();
});
it('keeps copied production values out of previews when public PR deployments are on', function (string $buildPack) {
[$application, $server] = makeDeploymentControlVarFixture([
'build_pack' => $buildPack,
'docker_compose_raw' => "services:\n app:\n image: nginx\n",
]);
$application->settings()->update(['is_pr_deployments_public_enabled' => true]);
createApplicationEnvironmentVariable($application, ['key' => 'DB_PASSWORD', 'value' => 'production-secret']);
createApplicationEnvironmentVariable($application, ['key' => 'API_URL', 'value' => 'https://api.example.com']);
createApplicationEnvironmentVariable($application, ['key' => 'PORT', 'value' => '3000']);
$application->environment_variables_preview()->where('key', 'API_URL')->first()
->update(['value' => 'https://preview-api.example.com']);
[$job, $reflection] = makeControlVarFilteringJob($application->fresh(), $server, [
'pull_request_id' => 99,
'remote_secrets_cache' => [],
]);
/** @var Collection $runtimeEnvs */
$runtimeEnvs = invokeDeploymentJobMethod($job, $reflection, 'generate_runtime_environment_variables');
/** @var Collection $buildtimeEnvs */
$buildtimeEnvs = invokeDeploymentJobMethod($job, $reflection, 'generate_buildtime_environment_variables');
invokeDeploymentJobMethod($job, $reflection, 'generate_env_variables');
/** @var Collection $envArgs */
$envArgs = readDeploymentJobProperty($job, $reflection, 'env_args');
/** @var Collection $railpackVariables */
$railpackVariables = invokeDeploymentJobMethod($job, $reflection, 'railpack_build_variables');
foreach ([$runtimeEnvs, $buildtimeEnvs] as $envLines) {
expect($envLines->filter(fn (string $env) => str($env)->contains('production-secret')))->toBeEmpty()
->and($envLines->contains(fn (string $env) => str($env)->startsWith('API_URL=') && str($env)->contains('preview-api.example.com')))->toBeTrue();
}
expect($envArgs->has('DB_PASSWORD'))->toBeFalse()
->and($envArgs->get('API_URL'))->toBe('https://preview-api.example.com')
->and($railpackVariables->has('DB_PASSWORD'))->toBeFalse()
->and($railpackVariables->get('API_URL'))->toBe('https://preview-api.example.com');
if ($buildPack !== 'dockercompose') {
expect($runtimeEnvs)->toContain('PORT='.$application->main_port()[0]);
}
})->with(['dockerfile', 'dockercompose']);
it('keeps copied production values for previews when public PR deployments are off', function () {
[$application, $server] = makeDeploymentControlVarFixture();
createApplicationEnvironmentVariable($application, ['key' => 'DB_PASSWORD', 'value' => 'production-secret']);
[$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('DB_PASSWORD=')))->toBeTrue();
});
it('keeps buildpack variables read at container start in the runtime env', function (int $pullRequestId, bool $isPreview) {
@@ -1720,39 +1791,6 @@ it('writes compose-safe preview runtime env files for user values', function ()
->toContain('PREVIEW_QUOTED_VALUE='.escapeComposeEnvFileValue('preview quotes" \'$`'));
});
it('writes compose-safe production fallbacks to preview runtime env files', function () {
[$application, $server] = makeDeploymentControlVarFixture();
createApplicationEnvironmentVariable($application, [
'key' => 'PREVIEW_TRIGGER',
'value' => 'preview-value',
'is_preview' => true,
'is_literal' => false,
'is_runtime' => true,
'is_buildtime' => false,
]);
createApplicationEnvironmentVariable($application, [
'key' => 'FALLBACK_QUOTED_VALUE',
'value' => 'fallback quotes" \'$`',
'is_literal' => true,
'is_runtime' => true,
'is_buildtime' => false,
]);
$application->environment_variables_preview()
->where('key', 'FALLBACK_QUOTED_VALUE')
->delete();
[$job, $reflection] = makeControlVarFilteringJob($application->fresh(), $server, [
'pull_request_id' => 42,
]);
/** @var Collection $runtimeEnvs */
$runtimeEnvs = invokeDeploymentJobMethod($job, $reflection, 'generate_runtime_environment_variables');
expect($runtimeEnvs)
->toContain('FALLBACK_QUOTED_VALUE='.escapeComposeEnvFileValue('fallback quotes" \'$`'));
});
it('writes compose-safe runtime env values for remote secret references', function () {
[$application, $server] = makeDeploymentControlVarFixture();