fix(deployment): detect application configuration changes consistently

Expand configuration snapshots, handle defaults from older snapshots, and refresh configuration state after Livewire setting changes.
This commit is contained in:
Andras Bacsai committed 2026-07-14 12:54:31 +02:00
1 parent 5381287606
commit 99e255a572
16 files changed
+415 -11

No files matched your search

@@ -102,6 +102,8 @@ class ApplicationConfigurationSnapshot
$this->item('git_repository', 'Repository', $this->application->git_repository, 'build'),
$this->item('git_branch', 'Branch', $this->application->git_branch, 'build'),
$this->item('git_commit_sha', 'Commit SHA', $this->application->git_commit_sha, 'build'),
$this->item('source_id', 'Source ID', $this->application->source_id, 'build'),
$this->item('source_type', 'Source type', $this->application->source_type, 'build'),
$this->item('private_key_id', 'Private key', $this->application->private_key_id, 'build'),
];
}
@@ -113,6 +115,8 @@ class ApplicationConfigurationSnapshot
{
return [
$this->item('build_pack', 'Build pack', $this->application->build_pack, 'build'),
$this->item('is_static', 'Static site', data_get($this->application, 'settings.is_static'), 'build'),
$this->item('is_spa', 'Single-page application', data_get($this->application, 'settings.is_spa'), 'build'),
$this->item('static_image', 'Static image', $this->application->static_image, 'build'),
$this->item('base_directory', 'Base directory', $this->application->base_directory, 'build'),
$this->item('publish_directory', 'Publish directory', $this->application->publish_directory, 'build'),
@@ -127,7 +131,11 @@ class ApplicationConfigurationSnapshot
// so comparing it would flag a permanent change for git-based compose apps.
$this->item('docker_compose_raw', 'Docker Compose', $this->application->docker_compose_raw, 'build', displayValue: $this->summarizeText($this->application->docker_compose_raw), displayFull: $this->application->docker_compose_raw, diffMode: 'lines'),
$this->item('docker_compose_custom_build_command', 'Docker Compose custom build command', $this->application->docker_compose_custom_build_command, 'build'),
$this->item('custom_docker_run_options', 'Custom Docker run options', $this->application->custom_docker_run_options, 'build'),
$this->item('is_git_submodules_enabled', 'Git submodules', data_get($this->application, 'settings.is_git_submodules_enabled'), 'build'),
$this->item('is_git_lfs_enabled', 'Git LFS', data_get($this->application, 'settings.is_git_lfs_enabled'), 'build'),
$this->item('is_git_shallow_clone_enabled', 'Shallow clone', data_get($this->application, 'settings.is_git_shallow_clone_enabled'), 'build'),
$this->item('is_env_sorting_enabled', 'Sort environment variables', data_get($this->application, 'settings.is_env_sorting_enabled'), 'build'),
$this->item('custom_docker_run_options', 'Custom Docker run options', $this->application->custom_docker_run_options, 'redeploy'),
$this->item('use_build_secrets', 'Use build secrets', data_get($this->application, 'settings.use_build_secrets'), 'build'),
$this->item('inject_build_args_to_dockerfile', 'Inject build args to Dockerfile', data_get($this->application, 'settings.inject_build_args_to_dockerfile'), 'build'),
$this->item('include_source_commit_in_build', 'Include source commit in build', data_get($this->application, 'settings.include_source_commit_in_build'), 'build'),
@@ -142,13 +150,26 @@ class ApplicationConfigurationSnapshot
private function runtimeItems(): array
{
return [
$this->item('docker_registry_image_name', 'Docker image', $this->application->docker_registry_image_name, 'redeploy'),
$this->item('docker_registry_image_tag', 'Docker image tag or hash', $this->application->docker_registry_image_tag, 'redeploy'),
$this->item('start_command', 'Start command', $this->application->start_command, 'redeploy'),
$this->item('pre_deployment_command', 'Pre-deployment command', $this->application->pre_deployment_command, 'redeploy'),
$this->item('pre_deployment_command_container', 'Pre-deployment command container', $this->application->pre_deployment_command_container, 'redeploy'),
$this->item('post_deployment_command', 'Post-deployment command', $this->application->post_deployment_command, 'redeploy'),
$this->item('post_deployment_command_container', 'Post-deployment command container', $this->application->post_deployment_command_container, 'redeploy'),
$this->item('docker_compose_custom_start_command', 'Docker Compose custom start command', $this->application->docker_compose_custom_start_command, 'redeploy'),
$this->item('ports_exposes', 'Exposed ports', $this->application->ports_exposes, 'redeploy'),
$this->item('ports_mappings', 'Port mappings', $this->application->ports_mappings, 'redeploy'),
$this->item('custom_network_aliases', 'Network aliases', $this->application->custom_network_aliases, 'redeploy'),
$this->item('connect_to_docker_network', 'Connect to Docker network', data_get($this->application, 'settings.connect_to_docker_network'), 'redeploy'),
$this->item('custom_internal_name', 'Custom container name', data_get($this->application, 'settings.custom_internal_name'), 'redeploy'),
$this->item('is_consistent_container_name_enabled', 'Consistent container name', data_get($this->application, 'settings.is_consistent_container_name_enabled'), 'redeploy'),
$this->item('is_container_label_escape_enabled', 'Escape container labels', data_get($this->application, 'settings.is_container_label_escape_enabled'), 'redeploy'),
$this->item('is_container_label_readonly_enabled', 'Read-only container labels', data_get($this->application, 'settings.is_container_label_readonly_enabled'), 'redeploy'),
$this->item('is_log_drain_enabled', 'Log drain', data_get($this->application, 'settings.is_log_drain_enabled'), 'redeploy'),
$this->item('is_swarm_only_worker_nodes', 'Swarm worker nodes only', data_get($this->application, 'settings.is_swarm_only_worker_nodes'), 'redeploy'),
$this->item('stop_grace_period', 'Stop grace period', $this->normalizedStopGracePeriod(), 'redeploy'),
$this->item('is_preserve_repository_enabled', 'Preserve repository', data_get($this->application, 'settings.is_preserve_repository_enabled'), 'redeploy'),
$this->item('is_raw_compose_deployment_enabled', 'Raw Compose deployment', data_get($this->application, 'settings.is_raw_compose_deployment_enabled'), 'redeploy'),
$this->item('is_gpu_enabled', 'GPU enabled', data_get($this->application, 'settings.is_gpu_enabled'), 'redeploy'),
$this->item('gpu_driver', 'GPU driver', data_get($this->application, 'settings.gpu_driver'), 'redeploy'),
@@ -170,7 +191,7 @@ class ApplicationConfigurationSnapshot
$this->item('docker_compose_domains', 'Service domains', $this->decodedComposeDomains(), 'redeploy', displayValue: $this->summarizeText($this->composeDomainsText()), displayFull: $this->composeDomainsText(), diffMode: 'lines'),
$this->item('redirect', 'Redirect', $this->application->redirect, 'redeploy'),
$this->item('custom_labels', 'Container labels', $this->application->custom_labels, 'redeploy', displayValue: $this->summarizeText($this->decodeCustomLabels($this->application->custom_labels)), displayFull: $this->decodeCustomLabels($this->application->custom_labels), diffMode: 'lines'),
$this->item('custom_nginx_configuration', 'Custom Nginx configuration', $this->application->custom_nginx_configuration, 'redeploy', displayValue: $this->summarizeText($this->application->custom_nginx_configuration), displayFull: $this->application->custom_nginx_configuration),
$this->item('custom_nginx_configuration', 'Custom Nginx configuration', $this->application->custom_nginx_configuration, 'build', displayValue: $this->summarizeText($this->application->custom_nginx_configuration), displayFull: $this->application->custom_nginx_configuration),
$this->item('is_force_https_enabled', 'Force HTTPS', data_get($this->application, 'settings.is_force_https_enabled'), 'redeploy'),
$this->item('is_gzip_enabled', 'Gzip', data_get($this->application, 'settings.is_gzip_enabled'), 'redeploy'),
$this->item('is_stripprefix_enabled', 'Strip prefix', data_get($this->application, 'settings.is_stripprefix_enabled'), 'redeploy'),
@@ -327,6 +348,17 @@ class ApplicationConfigurationSnapshot
return $flags ? "Hidden ({$flags})" : 'Hidden';
}
private function normalizedStopGracePeriod(): ?int
{
$stopGracePeriod = data_get($this->application, 'settings.stop_grace_period');
if ($stopGracePeriod === null || (int) $stopGracePeriod === DEFAULT_STOP_GRACE_PERIOD_SECONDS) {
return null;
}
return (int) $stopGracePeriod;
}
private function environmentFlags(EnvironmentVariable $environmentVariable): string
{
return collect([
@@ -17,6 +17,28 @@ class ConfigurationDiffer
*/
private const IGNORED_KEYS = ['build.docker_compose'];
/**
* Defaults for fields introduced after configuration snapshots were first
* stored. Older snapshots omitted these keys, which should not make an
* unchanged default look like a pending configuration change.
*
* @var array<string, bool|array<int, bool>>
*/
private const INTRODUCED_DEFAULTS = [
'build.is_static' => false,
'build.is_spa' => false,
'build.is_git_submodules_enabled' => true,
'build.is_git_lfs_enabled' => true,
'build.is_git_shallow_clone_enabled' => true,
'build.is_env_sorting_enabled' => [false, true],
'runtime.is_consistent_container_name_enabled' => false,
'runtime.is_container_label_escape_enabled' => true,
'runtime.is_container_label_readonly_enabled' => true,
'runtime.is_log_drain_enabled' => false,
'runtime.is_swarm_only_worker_nodes' => true,
'runtime.is_preserve_repository_enabled' => false,
];
/**
* @param array<string, mixed> $previousSnapshot
* @param array<string, mixed> $currentSnapshot
@@ -36,6 +58,14 @@ class ConfigurationDiffer
$previous = $previousItems[$key] ?? null;
$current = $currentItems[$key] ?? null;
if (
$previous === null
&& array_key_exists($key, self::INTRODUCED_DEFAULTS)
&& in_array((bool) data_get($current, 'compare_value'), (array) self::INTRODUCED_DEFAULTS[$key], true)
) {
continue;
}
if (($previous['compare_value'] ?? null) === ($current['compare_value'] ?? null)) {
continue;
}