diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index be7d747c46..42b2fc4831 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -1536,6 +1536,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->rolling_update(); return true; + } elseif ($configurationDiff->isLegacyFallback()) { + $this->application_deployment_queue->addLogEntry('The last deployment stored no configuration snapshot. Rebuilding image.'); } else { $this->application_deployment_queue->addLogEntry('Build configuration changed. Rebuilding image.'); } diff --git a/app/Models/Application.php b/app/Models/Application.php index bc59218bce..cd8d33893f 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -1491,23 +1491,13 @@ class Application extends BaseModel public function pendingDeploymentConfigurationDiff(): ConfigurationDiff { - $currentSnapshot = $this->deploymentConfigurationSnapshot(); - $lastDeployment = $this->get_last_successful_deployment(); - - $previousSnapshot = $lastDeployment?->configuration_snapshot; + $previousSnapshot = $this->get_last_successful_deployment()?->configuration_snapshot; if (! $previousSnapshot) { - $oldConfigHash = data_get($this, 'config_hash'); - $hasLegacyChange = $oldConfigHash === null || $oldConfigHash !== $this->legacyConfigurationHash(); - - if (! $hasLegacyChange) { - return ConfigurationDiff::unchanged(); - } - - $previousSnapshot = []; + return ConfigurationDiff::legacy(); } - return app(ConfigurationDiffer::class)->diff(ApplicationConfigurationSnapshot::withoutPreviewVolumes($previousSnapshot, $this), $currentSnapshot); + return app(ConfigurationDiffer::class)->diff(ApplicationConfigurationSnapshot::withoutPreviewVolumes($previousSnapshot, $this), $this->deploymentConfigurationSnapshot()); } public function hasPendingDeploymentConfigurationChanges(): bool @@ -1530,7 +1520,7 @@ class Application extends BaseModel $this->refresh(); if (! $deployment) { - $this->forceFill(['config_hash' => $this->legacyConfigurationHash()])->save(); + $this->forceFill(['config_hash' => $this->deploymentConfigurationHash()])->save(); return; } @@ -1558,18 +1548,6 @@ class Application extends BaseModel $this->forceFill(['config_hash' => $hash])->save(); } - private function legacyConfigurationHash(): string - { - $newConfigHash = base64_encode($this->fqdn.json_encode($this->noindexDomains()->all()).$this->git_repository.$this->git_branch.$this->git_commit_sha.$this->build_pack.$this->static_image.$this->install_command.$this->build_command.$this->start_command.$this->ports_exposes.$this->ports_mappings.$this->custom_network_aliases.$this->base_directory.$this->publish_directory.$this->dockerfile.$this->dockerfile_location.$this->custom_labels.$this->custom_docker_run_options.$this->dockerfile_target_build.$this->redirect.$this->custom_nginx_configuration.$this->settings?->use_build_secrets.$this->settings?->inject_build_args_to_dockerfile.$this->settings?->include_source_commit_in_build); - if ($this->pull_request_id === 0 || $this->pull_request_id === null) { - $newConfigHash .= json_encode($this->environment_variables()->get(['value', 'is_multiline', 'is_literal', 'is_buildtime', 'is_runtime'])->makeVisible('value')->sort()); - } else { - $newConfigHash .= json_encode($this->environment_variables_preview()->get(['value', 'is_multiline', 'is_literal', 'is_buildtime', 'is_runtime'])->makeVisible('value')->sort()); - } - - return md5($newConfigHash); - } - public function customRepository() { return convertGitUrl($this->git_repository, $this->deploymentType(), $this->source); diff --git a/app/Services/DeploymentConfiguration/ConfigurationDiff.php b/app/Services/DeploymentConfiguration/ConfigurationDiff.php index 3f0477ba35..b648331c53 100644 --- a/app/Services/DeploymentConfiguration/ConfigurationDiff.php +++ b/app/Services/DeploymentConfiguration/ConfigurationDiff.php @@ -17,25 +17,13 @@ class ConfigurationDiff return new self; } - public static function legacy(bool $changed): self + /** + * The last deployment stored no configuration snapshot (it ran before v4.1), so the + * deployed configuration is unknown. Report no changes, but build on the next deployment. + */ + public static function legacy(): self { - if (! $changed) { - return self::unchanged(); - } - - return new self([ - [ - 'key' => 'legacy.configuration', - 'section' => 'configuration', - 'section_label' => 'Configuration', - 'label' => 'Configuration', - 'type' => 'changed', - 'impact' => 'build', - 'sensitive' => false, - 'old_display_value' => 'Previously deployed configuration', - 'new_display_value' => 'Current configuration', - ], - ], true); + return new self(legacyFallback: true); } /** @@ -63,7 +51,7 @@ class ConfigurationDiff public function requiresBuild(): bool { - return collect($this->changes)->contains(fn (array $change): bool => $change['impact'] === 'build'); + return $this->legacyFallback || collect($this->changes)->contains(fn (array $change): bool => $change['impact'] === 'build'); } public function requiresRedeploy(): bool diff --git a/tests/Feature/ApplicationConfigurationChangedTest.php b/tests/Feature/ApplicationConfigurationChangedTest.php index 4650e2b12c..9c21e48fda 100644 --- a/tests/Feature/ApplicationConfigurationChangedTest.php +++ b/tests/Feature/ApplicationConfigurationChangedTest.php @@ -135,13 +135,12 @@ it('does not flag applications whose older snapshot omitted noindex domains', fu expect($application->refresh()->pendingDeploymentConfigurationDiff()->isChanged())->toBeFalse(); }); -it('checks legacy preview deployment configuration hash using preview environment variable query', function () { +it('does not report changes for applications deployed before configuration snapshots existed', function () { $application = configurationChangedTestApplication(); EnvironmentVariable::create([ - 'key' => 'APP_ENV', - 'value' => 'preview', - 'is_preview' => true, + 'key' => 'APP_SECRET', + 'value' => 'secret', 'is_multiline' => false, 'is_literal' => false, 'is_buildtime' => true, @@ -150,29 +149,28 @@ it('checks legacy preview deployment configuration hash using preview environmen 'resourceable_id' => $application->id, ]); - $application->forceFill([ - 'config_hash' => 'legacy-hash', - 'pull_request_id' => 123, - ]); + configurationChangedDeployment($application); + $application->forceFill(['config_hash' => md5('hash written by an older Coolify version')])->save(); - $diff = $application->pendingDeploymentConfigurationDiff(); + $diff = $application->refresh()->pendingDeploymentConfigurationDiff(); - expect($diff->isChanged())->toBeTrue() - ->and($diff->count())->toBeGreaterThan(0); + expect($diff->isChanged())->toBeFalse() + ->and($diff->count())->toBe(0) + ->and($diff->isLegacyFallback())->toBeTrue() + ->and($diff->requiresBuild())->toBeTrue(); }); -it('falls back to real diff against empty snapshot when no deployment snapshot exists', function () { +it('compares with the deployment snapshot once an application without one is deployed again', function () { $application = configurationChangedTestApplication(); - $application->isConfigurationChanged(save: true); - - expect($application->refresh()->pendingDeploymentConfigurationDiff()->isChanged())->toBeFalse(); + configurationChangedDeployment($application); + $application->forceFill(['config_hash' => md5('hash written by an older Coolify version')])->save(); + $application->markDeploymentConfigurationApplied(configurationChangedDeployment($application)); $application->update(['build_command' => 'pnpm build']); $diff = $application->refresh()->pendingDeploymentConfigurationDiff(); - expect($diff->isChanged())->toBeTrue() - ->and($diff->isLegacyFallback())->toBeFalse() - ->and($diff->count())->toBeGreaterThan(0) - ->and(collect($diff->changes())->pluck('label')->toArray())->toContain('Build command'); + expect($diff->isLegacyFallback())->toBeFalse() + ->and(collect($diff->changes())->pluck('label')->all())->toBe(['Build command']) + ->and($diff->requiresBuild())->toBeTrue(); });