fix(previews): pass production runtime variables without a preview copy

Previews from v4.5.1 on did not get production runtime variables that
have no preview variable. They get them again, except when public PR
deployments are on. The fallback skips keys that have a preview
variable, also when Runtime is off on it.

Each preview deployment now logs the variables that it does not get:
copies with the production value that public PR deployments skip, and
production variables that have no preview variable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai
2026-11-10 13:14:49 +01:00
co-authored by Claude Opus 5.5
parent edfa45aae2
commit c49d9f623b
3 changed files with 79 additions and 10 deletions
@@ -0,0 +1,6 @@
---
type: fixed
---
Missing production runtime variables in preview deployments when the variable has no preview variable
- A preview gets these production runtime variables again, except when `PR deployment access` is public. The deployment log lists each variable that a preview does not get
+65 -6
View File
@@ -461,6 +461,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
try {
$this->validateDeploymentEnvironmentVariableKeys();
$this->warnAboutPreviewVariablesNotPassed();
$this->ensureRegistryImageForMultipleServers();
$this->ensureRegistryImageIsNotShared();
$this->warnAboutVolumesOnMultipleServers();
@@ -2184,9 +2185,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$envs->push($this->runtimeEnvironmentAssignment($env));
}
// A preview from before v4.5.1 also gets the production variables that have no preview copy.
if ($this->preview?->keeps_production_values && $runtime_environment_variables_preview->isNotEmpty()) {
$previewKeys = $runtime_environment_variables_preview->pluck('key')->all();
// The production runtime variables that have no preview variable.
if ($this->previewGetsProductionRuntimeFallback()) {
$previewKeys = $this->application->environment_variables_preview()->pluck('key')->all();
$sorted_environment_variables
->filter(fn (EnvironmentVariable $env) => $env->is_runtime && ! in_array($env->key, $previewKeys, true) && ! $this->shouldOmitBlankComposeEnvironmentVariable($env))
->each(fn (EnvironmentVariable $env) => $envs->push($this->runtimeEnvironmentAssignment($env)));
@@ -2194,12 +2195,12 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
// Add PORT if not exists, use the first port as default
if ($this->build_pack !== 'dockercompose') {
if ($this->deployedPreviewVariables()->where('key', 'PORT')->isEmpty()) {
if ($this->deployedPreviewVariables()->where('key', 'PORT')->isEmpty() && ! $envs->contains(fn (string $env) => str_starts_with($env, 'PORT='))) {
$envs->push("PORT={$ports[0]}");
}
}
// Add HOST if not exists
if ($this->deployedPreviewVariables()->where('key', 'HOST')->isEmpty()) {
if ($this->deployedPreviewVariables()->where('key', 'HOST')->isEmpty() && ! $envs->contains(fn (string $env) => str_starts_with($env, 'HOST='))) {
$envs->push('HOST=0.0.0.0');
}
}
@@ -2805,6 +2806,64 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->application_deployment_queue->addLogEntry(' Suggested name: '.ValidationPatterns::displayShellEnvironmentVariableKey($suggestedKey), type: 'info');
}
/**
* A preview can run code from a fork when public PR deployments are on, so it must not get production
* values. A preview from before v4.5.1 keeps them, because it was deployed with them.
*/
private function previewUsesProductionValues(): bool
{
return ! $this->application->settings->is_pr_deployments_public_enabled || (bool) $this->preview?->keeps_production_values;
}
/**
* A preview gets the production runtime variables that have no preview variable, as before v4.5.1.
* This needs at least one runtime preview variable, so previews without preview variables stay
* without production values.
*/
private function previewGetsProductionRuntimeFallback(): bool
{
return $this->pull_request_id !== 0
&& $this->previewUsesProductionValues()
&& $this->application->runtime_environment_variables_preview()->where('is_runtime', true)->exists();
}
/**
* Log the variables that this preview deployment does not get, so users know what to add.
*/
private function warnAboutPreviewVariablesNotPassed(): void
{
if ($this->pull_request_id === 0) {
return;
}
$previewVariables = $this->application->environment_variables_preview()->get();
$skippedKeys = $previewVariables->whereIn('id', $this->previewVariableIdsWithProductionValues())->pluck('key');
if ($skippedKeys->isNotEmpty()) {
$this->application_deployment_queue->addLogEntry('⚠️ Public PR deployments are on, so this preview does not get the preview variables that still have the production value: '.$skippedKeys->implode(', '), 'stderr');
$this->application_deployment_queue->addLogEntry(' To use them, give them a preview value in Environment Variables > Preview Deployments.', type: 'info');
}
$getsRuntimeFallback = $this->previewGetsProductionRuntimeFallback();
$passesBuildtimeVariables = $this->deploymentPassesBuildtimeVariables();
$missingKeys = $this->application->environment_variables()->get()
->reject(fn (EnvironmentVariable $env) => $previewVariables->contains('key', $env->key))
->reject(fn (EnvironmentVariable $env) => $this->build_pack === 'dockercompose' && $this->isGeneratedDockerComposeEnvironmentVariable($env))
->map(function (EnvironmentVariable $env) use ($getsRuntimeFallback, $passesBuildtimeVariables): ?string {
$missingAtRuntime = $env->is_runtime && ! $getsRuntimeFallback;
$missingAtBuildtime = $env->is_buildtime && $passesBuildtimeVariables;
if (! $missingAtRuntime && ! $missingAtBuildtime) {
return null;
}
return $env->is_runtime && ! $missingAtRuntime ? "{$env->key} (at build time)" : $env->key;
})
->filter();
if ($missingKeys->isNotEmpty()) {
$this->application_deployment_queue->addLogEntry('⚠️ These production variables have no preview variable, so this preview does not get them: '.$missingKeys->implode(', '), 'stderr');
$this->application_deployment_queue->addLogEntry(' To use them, add them in Environment Variables > Preview Deployments.', type: 'info');
}
}
/**
* IDs of preview variables that have the same value as the production variable with the same key.
* Coolify copies each new production variable to the previews. With public PR deployments, a
@@ -2818,7 +2877,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
return $this->previewVariableIdsWithProductionValues;
}
if ($this->pull_request_id === 0 || ! $this->application->settings->is_pr_deployments_public_enabled || $this->preview?->keeps_production_values) {
if ($this->pull_request_id === 0 || $this->previewUsesProductionValues()) {
return $this->previewVariableIdsWithProductionValues = [];
}
@@ -948,8 +948,9 @@ it('filters docker compose generated service variables from build args', functio
expect($envArgs->has('SERVICE_URL_APP'))->toBeFalse();
});
it('does not fall back to production vars when preview copies are deleted', function () {
it('falls back to production runtime vars without a preview copy only when public PR deployments are off', function (bool $isPublic) {
[$application, $server] = makeDeploymentControlVarFixture();
$application->settings()->update(['is_pr_deployments_public_enabled' => $isPublic]);
createApplicationEnvironmentVariable($application, [
'key' => 'APP_NAME',
@@ -967,16 +968,19 @@ it('does not fall back to production vars when preview copies are deleted', func
$application->environment_variables_preview()->where('key', 'APP_NAME')->delete();
[$job, $reflection] = makeControlVarFilteringJob($application, $server, [
[$job, $reflection] = makeControlVarFilteringJob($application->fresh(), $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=')))->toBeFalse();
expect($runtimeEnvs->contains(fn (string $env) => str($env)->startsWith('APP_NAME=')))->toBe(! $isPublic);
expect($runtimeEnvs->contains(fn (string $env) => str($env)->startsWith('PREVIEW_FLAG=')))->toBeTrue();
});
})->with([
'public PR deployments off' => [false],
'public PR deployments on' => [true],
]);
it('does not give production runtime values to a preview copy that is not a runtime variable', function () {
[$application, $server] = makeDeploymentControlVarFixture([