diff --git a/.changes/missing-production-runtime-variables-in-preview.md b/.changes/missing-production-runtime-variables-in-preview.md new file mode 100644 index 0000000000..acda6f98fb --- /dev/null +++ b/.changes/missing-production-runtime-variables-in-preview.md @@ -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 diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 8d68902ffa..2c017aa9dd 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -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 = []; } diff --git a/tests/Feature/ApplicationDeploymentControlVarFilteringTest.php b/tests/Feature/ApplicationDeploymentControlVarFilteringTest.php index 9ad41b1846..3db71b3135 100644 --- a/tests/Feature/ApplicationDeploymentControlVarFilteringTest.php +++ b/tests/Feature/ApplicationDeploymentControlVarFilteringTest.php @@ -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([