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:
parent
5381287606
commit
99e255a572
16 changed files with 415 additions and 11 deletions
|
|
@ -286,6 +286,7 @@ public function saveStopGracePeriod()
|
|||
$this->application->settings->save();
|
||||
|
||||
$this->dispatch('success', 'Stop grace period updated.');
|
||||
$this->dispatch('configurationChanged');
|
||||
} catch (ValidationException $e) {
|
||||
throw $e;
|
||||
} catch (\Throwable $e) {
|
||||
|
|
|
|||
|
|
@ -489,6 +489,7 @@ public function instantSave()
|
|||
if ($this->isContainerLabelReadonlyEnabled) {
|
||||
$this->resetDefaultLabels(false);
|
||||
}
|
||||
$this->dispatch('configurationChanged');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -147,6 +147,7 @@ public function changeSource($sourceId, $sourceType)
|
|||
'source_id' => $source->id,
|
||||
'source_type' => $sourceType,
|
||||
]);
|
||||
$this->dispatch('configurationChanged');
|
||||
|
||||
['repository' => $customRepository] = $this->application->customRepository();
|
||||
$repository = githubApi($this->application->source, "repos/{$customRepository}");
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ public function instantSave()
|
|||
$this->authorize('update', $this->application);
|
||||
$this->syncData(true);
|
||||
$this->dispatch('success', 'Swarm settings updated.');
|
||||
$this->dispatch('configurationChanged');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
|
@ -68,6 +69,7 @@ public function submit()
|
|||
$this->authorize('update', $this->application);
|
||||
$this->syncData(true);
|
||||
$this->dispatch('success', 'Swarm settings updated.');
|
||||
$this->dispatch('configurationChanged');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ public function instantSave()
|
|||
$this->resource->settings->save();
|
||||
$this->getDevView();
|
||||
$this->dispatch('success', 'Environment variable settings updated.');
|
||||
$this->dispatch('configurationChanged');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -152,6 +152,7 @@ public function instantSave()
|
|||
$this->resource->custom_healthcheck_found = $this->customHealthcheckFound;
|
||||
$this->resource->save();
|
||||
$this->dispatch('success', 'Health check updated.');
|
||||
$this->dispatch('configurationChanged');
|
||||
}
|
||||
|
||||
public function submit()
|
||||
|
|
@ -178,6 +179,7 @@ public function submit()
|
|||
$this->resource->custom_healthcheck_found = $this->customHealthcheckFound;
|
||||
$this->resource->save();
|
||||
$this->dispatch('success', 'Health check updated.');
|
||||
$this->dispatch('configurationChanged');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
|
@ -213,6 +215,7 @@ public function toggleHealthcheck()
|
|||
} else {
|
||||
$this->dispatch('success', 'Health check '.($this->healthCheckEnabled ? 'enabled' : 'disabled').'.');
|
||||
}
|
||||
$this->dispatch('configurationChanged');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,6 +102,8 @@ private function sourceItems(): array
|
|||
$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 @@ private function buildItems(): array
|
|||
{
|
||||
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 @@ private function buildItems(): array
|
|||
// 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 @@ private function buildItems(): array
|
|||
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 @@ private function domainItems(): array
|
|||
$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 @@ private function environmentDisplayValue(EnvironmentVariable $environmentVariabl
|
|||
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 @@ public function diff(array $previousSnapshot, array $currentSnapshot): Configura
|
|||
$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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@
|
|||
"alexandrie": {
|
||||
"documentation": "https://github.com/Smaug6739/Alexandrie/tree/main/docs?utm_source=coolify.io",
|
||||
"slogan": "A powerful Markdown workspace designed for speed, clarity, and creativity.",
|
||||
"compose": "c2VydmljZXM6CiAgZnJvbnRlbmQ6CiAgICBpbWFnZTogJ2doY3IuaW8vc21hdWc2NzM5L2FsZXhhbmRyaWUtZnJvbnRlbmQ6djguNy4yJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9VUkxfRlJPTlRFTkRfODIwMAogICAgICAtIFBPUlQ9ODIwMAogICAgICAtICdOVVhUX1BVQkxJQ19DT05GSUdfRElTQUJMRV9TSUdOVVBfUEFHRT0ke0NPTkZJR19ESVNBQkxFX1NJR05VUDotZmFsc2V9JwogICAgICAtICdOVVhUX1BVQkxJQ19DT05GSUdfRElTQUJMRV9MQU5ESU5HX1BBR0U9JHtDT05GSUdfRElTQUJMRV9MQU5ESU5HOi1mYWxzZX0nCiAgICAgIC0gJ05VWFRfUFVCTElDX0JBU0VfQVBJPSR7U0VSVklDRV9VUkxfQkFDS0VORH0nCiAgICAgIC0gJ05VWFRfUFVCTElDX0JBU0VfQ0ROPSR7U0VSVklDRV9VUkxfUlVTVEZTfScKICAgICAgLSAnTlVYVF9QVUJMSUNfQ0ROX0VORFBPSU5UPSR7Q0ROX0VORFBPSU5UOi0vYWxleGFuZHJpZS99JwogICAgICAtICdOVVhUX1BVQkxJQ19CQVNFX1VSTD0ke1NFUlZJQ0VfVVJMX0ZST05URU5EfScKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYmFja2VuZAogIGJhY2tlbmQ6CiAgICBpbWFnZTogJ2doY3IuaW8vc21hdWc2NzM5L2FsZXhhbmRyaWUtYmFja2VuZDp2OC43LjInCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9CQUNLRU5EXzgyMDEKICAgICAgLSBCQUNLRU5EX1BPUlQ9ODIwMQogICAgICAtIEdJTl9NT0RFPXJlbGVhc2UKICAgICAgLSAnSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSldUfScKICAgICAgLSAnQ09PS0lFX0RPTUFJTj0ke1NFUlZJQ0VfVVJMX0ZST05URU5EfScKICAgICAgLSAnRlJPTlRFTkRfVVJMPSR7U0VSVklDRV9VUkxfRlJPTlRFTkR9JwogICAgICAtICdBTExPV19VTlNFQ1VSRT0ke0FMTE9XX1VOU0VDVVJFOi1mYWxzZX0nCiAgICAgIC0gREFUQUJBU0VfSE9TVD1teXNxbAogICAgICAtIERBVEFCQVNFX1BPUlQ9MzMwNgogICAgICAtICdEQVRBQkFTRV9OQU1FPSR7TVlTUUxfREFUQUJBU0U6LWFsZXhhbmRyaWUtZGJ9JwogICAgICAtICdEQVRBQkFTRV9VU0VSPSR7U0VSVklDRV9VU0VSX01ZU1FMfScKICAgICAgLSAnREFUQUJBU0VfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01ZU1FMfScKICAgICAgLSAnTUlOSU9fRU5EUE9JTlQ9cnVzdGZzOjkwMDAnCiAgICAgIC0gJ01JTklPX1BVQkxJQ19VUkw9JHtTRVJWSUNFX1VSTF9SVVNURlN9JwogICAgICAtICdNSU5JT19TRUNVUkU9JHtNSU5JT19TRUNVUkU6LWZhbHNlfScKICAgICAgLSAnTUlOSU9fQUNDRVNTS0VZPSR7U0VSVklDRV9VU0VSX1JVU1RGU30nCiAgICAgIC0gJ01JTklPX1NFQ1JFVEtFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfUlVTVEZTfScKICAgICAgLSAnTUlOSU9fQlVDS0VUPSR7TUlOSU9fQlVDS0VUOi1hbGV4YW5kcmllfScKICAgICAgLSAnU01UUF9IT1NUPSR7U01UUF9IT1NUOi19JwogICAgICAtICdTTVRQX01BSUw9JHtTTVRQX01BSUw6LX0nCiAgICAgIC0gJ1NNVFBfUEFTU1dPUkQ9JHtTTVRQX1BBU1NXT1JEOi19JwogICAgZGVwZW5kc19vbjoKICAgICAgbXlzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcnVzdGZzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgbXlzcWw6CiAgICBpbWFnZTogJ215c3FsOjguMCcKICAgIGVudmlyb25tZW50OgogICAgICAtICdNWVNRTF9ST09UX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9NWVNRTFJPT1R9JwogICAgICAtICdNWVNRTF9VU0VSPSR7U0VSVklDRV9VU0VSX01ZU1FMfScKICAgICAgLSAnTVlTUUxfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01ZU1FMfScKICAgICAgLSAnTVlTUUxfREFUQUJBU0U9JHtNWVNRTF9EQVRBQkFTRTotYWxleGFuZHJpZS1kYn0nCiAgICB2b2x1bWVzOgogICAgICAtICdteXNxbC1kYXRhOi92YXIvbGliL215c3FsJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIG15c3FsYWRtaW4KICAgICAgICAtIHBpbmcKICAgICAgICAtICctaCcKICAgICAgICAtIGxvY2FsaG9zdAogICAgICAgIC0gJy11JwogICAgICAgIC0gcm9vdAogICAgICAgIC0gJy1wJHtTRVJWSUNFX1BBU1NXT1JEX01ZU1FMUk9PVH0nCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgcmV0cmllczogNQogIHJ1c3RmczoKICAgIGltYWdlOiAncnVzdGZzL3J1c3RmczoxLjAuMC1hbHBoYS45MCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX1JVU1RGU185MDAwCiAgICAgIC0gJ1JVU1RGU19BQ0NFU1NfS0VZPSR7U0VSVklDRV9VU0VSX1JVU1RGU30nCiAgICAgIC0gJ1JVU1RGU19TRUNSRVRfS0VZPSR7U0VSVklDRV9QQVNTV09SRF9SVVNURlN9JwogICAgICAtICdSVVNURlNfQ09OU09MRV9FTkFCTEU9JHtSVVNURlNfQ09OU09MRV9FTkFCTEU6LWZhbHNlfScKICAgICAgLSAnUlVTVEZTX0xPR19MRVZFTD0ke1JVU1RGU19MT0dfTEVWRUw6LWluZm99JwogICAgdm9sdW1lczoKICAgICAgLSAncnVzdGZzLWRhdGE6L2RhdGEnCiAgICAgIC0gJ3J1c3Rmcy1sb2dzOi9sb2dzJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICduYyAteiBsb2NhbGhvc3QgOTAwMCB8fCBleGl0IDEnCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogNQo=",
|
||||
"compose": "c2VydmljZXM6CiAgZnJvbnRlbmQ6CiAgICBpbWFnZTogJ2doY3IuaW8vc21hdWc2NzM5L2FsZXhhbmRyaWUtZnJvbnRlbmQ6djguMTAuMCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX0ZST05URU5EXzgyMDAKICAgICAgLSBQT1JUPTgyMDAKICAgICAgLSAnTlVYVF9QVUJMSUNfQ09ORklHX0RJU0FCTEVfU0lHTlVQX1BBR0U9JHtDT05GSUdfRElTQUJMRV9TSUdOVVA6LWZhbHNlfScKICAgICAgLSAnTlVYVF9QVUJMSUNfQ09ORklHX0RJU0FCTEVfTEFORElOR19QQUdFPSR7Q09ORklHX0RJU0FCTEVfTEFORElORzotZmFsc2V9JwogICAgICAtICdOVVhUX1BVQkxJQ19CQVNFX0FQST0ke1NFUlZJQ0VfVVJMX0JBQ0tFTkR9JwogICAgICAtICdOVVhUX1BVQkxJQ19CQVNFX0NETj0ke1NFUlZJQ0VfVVJMX1JVU1RGU30nCiAgICAgIC0gJ05VWFRfUFVCTElDX0NETl9FTkRQT0lOVD0ke0NETl9FTkRQT0lOVDotL2FsZXhhbmRyaWUvfScKICAgICAgLSAnTlVYVF9QVUJMSUNfQkFTRV9VUkw9JHtTRVJWSUNFX1VSTF9GUk9OVEVORH0nCiAgICBkZXBlbmRzX29uOgogICAgICAtIGJhY2tlbmQKICBiYWNrZW5kOgogICAgaW1hZ2U6ICdnaGNyLmlvL3NtYXVnNjczOS9hbGV4YW5kcmllLWJhY2tlbmQ6djguMTAuMCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX0JBQ0tFTkRfODIwMQogICAgICAtIEJBQ0tFTkRfUE9SVD04MjAxCiAgICAgIC0gR0lOX01PREU9cmVsZWFzZQogICAgICAtICdKV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1R9JwogICAgICAtICdDT09LSUVfRE9NQUlOPSR7U0VSVklDRV9VUkxfRlJPTlRFTkR9JwogICAgICAtICdGUk9OVEVORF9VUkw9JHtTRVJWSUNFX1VSTF9GUk9OVEVORH0nCiAgICAgIC0gJ0FMTE9XX1VOU0VDVVJFPSR7QUxMT1dfVU5TRUNVUkU6LWZhbHNlfScKICAgICAgLSBEQVRBQkFTRV9IT1NUPW15c3FsCiAgICAgIC0gREFUQUJBU0VfUE9SVD0zMzA2CiAgICAgIC0gJ0RBVEFCQVNFX05BTUU9JHtNWVNRTF9EQVRBQkFTRTotYWxleGFuZHJpZS1kYn0nCiAgICAgIC0gJ0RBVEFCQVNFX1VTRVI9JHtTRVJWSUNFX1VTRVJfTVlTUUx9JwogICAgICAtICdEQVRBQkFTRV9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUx9JwogICAgICAtICdNSU5JT19FTkRQT0lOVD1ydXN0ZnM6OTAwMCcKICAgICAgLSAnTUlOSU9fUFVCTElDX1VSTD0ke1NFUlZJQ0VfVVJMX1JVU1RGU30nCiAgICAgIC0gJ01JTklPX1NFQ1VSRT0ke01JTklPX1NFQ1VSRTotZmFsc2V9JwogICAgICAtICdNSU5JT19BQ0NFU1NLRVk9JHtTRVJWSUNFX1VTRVJfUlVTVEZTfScKICAgICAgLSAnTUlOSU9fU0VDUkVUS0VZPSR7U0VSVklDRV9QQVNTV09SRF9SVVNURlN9JwogICAgICAtICdNSU5JT19CVUNLRVQ9JHtNSU5JT19CVUNLRVQ6LWFsZXhhbmRyaWV9JwogICAgICAtICdTTVRQX0hPU1Q9JHtTTVRQX0hPU1Q6LX0nCiAgICAgIC0gJ1NNVFBfTUFJTD0ke1NNVFBfTUFJTDotfScKICAgICAgLSAnU01UUF9QQVNTV09SRD0ke1NNVFBfUEFTU1dPUkQ6LX0nCiAgICBkZXBlbmRzX29uOgogICAgICBteXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBydXN0ZnM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICBteXNxbDoKICAgIGltYWdlOiAnbXlzcWw6OC4wJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ01ZU1FMX1JPT1RfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01ZU1FMUk9PVH0nCiAgICAgIC0gJ01ZU1FMX1VTRVI9JHtTRVJWSUNFX1VTRVJfTVlTUUx9JwogICAgICAtICdNWVNRTF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUx9JwogICAgICAtICdNWVNRTF9EQVRBQkFTRT0ke01ZU1FMX0RBVEFCQVNFOi1hbGV4YW5kcmllLWRifScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ215c3FsLWRhdGE6L3Zhci9saWIvbXlzcWwnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gbXlzcWxhZG1pbgogICAgICAgIC0gcGluZwogICAgICAgIC0gJy1oJwogICAgICAgIC0gbG9jYWxob3N0CiAgICAgICAgLSAnLXUnCiAgICAgICAgLSByb290CiAgICAgICAgLSAnLXAke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUxST09UfScKICAgICAgdGltZW91dDogNXMKICAgICAgaW50ZXJ2YWw6IDEwcwogICAgICByZXRyaWVzOiA1CiAgcnVzdGZzOgogICAgaW1hZ2U6ICdydXN0ZnMvcnVzdGZzOjEuMC4wLWJldGEuOCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX1JVU1RGU185MDAwCiAgICAgIC0gJ1JVU1RGU19BQ0NFU1NfS0VZPSR7U0VSVklDRV9VU0VSX1JVU1RGU30nCiAgICAgIC0gJ1JVU1RGU19TRUNSRVRfS0VZPSR7U0VSVklDRV9QQVNTV09SRF9SVVNURlN9JwogICAgICAtICdSVVNURlNfQ09OU09MRV9FTkFCTEU9JHtSVVNURlNfQ09OU09MRV9FTkFCTEU6LWZhbHNlfScKICAgICAgLSAnUlVTVEZTX0xPR19MRVZFTD0ke1JVU1RGU19MT0dfTEVWRUw6LWluZm99JwogICAgdm9sdW1lczoKICAgICAgLSAncnVzdGZzLWRhdGE6L2RhdGEnCiAgICAgIC0gJ3J1c3Rmcy1sb2dzOi9sb2dzJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICduYyAteiBsb2NhbGhvc3QgOTAwMCB8fCBleGl0IDEnCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogNQo=",
|
||||
"tags": [
|
||||
"note-taking",
|
||||
"markdown",
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
"category": "productivity",
|
||||
"logo": "svgs/alexandrie.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2026-04-05T13:36:24+02:00",
|
||||
"template_last_updated_at": "2026-07-07T13:24:35+02:00",
|
||||
"port": "8200"
|
||||
},
|
||||
"anythingllm": {
|
||||
|
|
@ -1310,7 +1310,7 @@
|
|||
"espocrm": {
|
||||
"documentation": "https://docs.espocrm.com?utm_source=coolify.io",
|
||||
"slogan": "EspoCRM is a free and open-source CRM platform.",
|
||||
"compose": "c2VydmljZXM6CiAgZXNwb2NybToKICAgIGltYWdlOiAnZXNwb2NybS9lc3BvY3JtOjknCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9FU1BPQ1JNCiAgICAgIC0gJ0VTUE9DUk1fQURNSU5fVVNFUk5BTUU9JHtFU1BPQ1JNX0FETUlOX1VTRVJOQU1FOi1hZG1pbn0nCiAgICAgIC0gJ0VTUE9DUk1fQURNSU5fUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0FETUlOfScKICAgICAgLSBFU1BPQ1JNX0RBVEFCQVNFX1BMQVRGT1JNPU15c3FsCiAgICAgIC0gRVNQT0NSTV9EQVRBQkFTRV9IT1NUPWVzcG9jcm0tZGIKICAgICAgLSAnRVNQT0NSTV9EQVRBQkFTRV9OQU1FPSR7TUFSSUFEQl9EQVRBQkFTRTotZXNwb2NybX0nCiAgICAgIC0gJ0VTUE9DUk1fREFUQUJBU0VfVVNFUj0ke1NFUlZJQ0VfVVNFUl9NQVJJQURCfScKICAgICAgLSAnRVNQT0NSTV9EQVRBQkFTRV9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQn0nCiAgICAgIC0gJ0VTUE9DUk1fU0lURV9VUkw9JHtTRVJWSUNFX1VSTF9FU1BPQ1JNfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2VzcG9jcm06L3Zhci93d3cvaHRtbCcKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo4MCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHN0YXJ0X3BlcmlvZDogNjBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogICAgZGVwZW5kc19vbjoKICAgICAgZXNwb2NybS1kYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogIGVzcG9jcm0tZGFlbW9uOgogICAgaW1hZ2U6ICdlc3BvY3JtL2VzcG9jcm06OScKICAgIGNvbnRhaW5lcl9uYW1lOiBlc3BvY3JtLWRhZW1vbgogICAgdm9sdW1lczoKICAgICAgLSAnZXNwb2NybTovdmFyL3d3dy9odG1sJwogICAgcmVzdGFydDogYWx3YXlzCiAgICBlbnRyeXBvaW50OiBkb2NrZXItZGFlbW9uLnNoCiAgICBkZXBlbmRzX29uOgogICAgICBlc3BvY3JtOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgZXNwb2NybS13ZWJzb2NrZXQ6CiAgICBpbWFnZTogJ2VzcG9jcm0vZXNwb2NybTo5JwogICAgY29udGFpbmVyX25hbWU6IGVzcG9jcm0td2Vic29ja2V0CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9FU1BPQ1JNX1dFQlNPQ0tFVF84MDgwCiAgICAgIC0gRVNQT0NSTV9DT05GSUdfVVNFX1dFQl9TT0NLRVQ9dHJ1ZQogICAgICAtIEVTUE9DUk1fQ09ORklHX1dFQl9TT0NLRVRfVVJMPSRTRVJWSUNFX1VSTF9FU1BPQ1JNX1dFQlNPQ0tFVAogICAgICAtICdFU1BPQ1JNX0NPTkZJR19XRUJfU09DS0VUX1pFUk9fTV9RX1NVQlNDUklCRVJfRFNOPXRjcDovLyo6Nzc3NycKICAgICAgLSAnRVNQT0NSTV9DT05GSUdfV0VCX1NPQ0tFVF9aRVJPX01fUV9TVUJNSVNTSU9OX0RTTj10Y3A6Ly9lc3BvY3JtLXdlYnNvY2tldDo3Nzc3JwogICAgdm9sdW1lczoKICAgICAgLSAnZXNwb2NybTovdmFyL3d3dy9odG1sJwogICAgcmVzdGFydDogYWx3YXlzCiAgICBlbnRyeXBvaW50OiBkb2NrZXItd2Vic29ja2V0LnNoCiAgICBkZXBlbmRzX29uOgogICAgICBlc3BvY3JtOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgZXNwb2NybS1kYjoKICAgIGltYWdlOiAnbWFyaWFkYjoxMS44JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ01BUklBREJfREFUQUJBU0U9JHtNQVJJQURCX0RBVEFCQVNFOi1lc3BvY3JtfScKICAgICAgLSAnTUFSSUFEQl9VU0VSPSR7U0VSVklDRV9VU0VSX01BUklBREJ9JwogICAgICAtICdNQVJJQURCX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9NQVJJQURCfScKICAgICAgLSAnTUFSSUFEQl9ST09UX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9ST09UfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2VzcG9jcm0tZGI6L3Zhci9saWIvbXlzcWwnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gaGVhbHRoY2hlY2suc2gKICAgICAgICAtICctLWNvbm5lY3QnCiAgICAgICAgLSAnLS1pbm5vZGJfaW5pdGlhbGl6ZWQnCiAgICAgIGludGVydmFsOiAyMHMKICAgICAgc3RhcnRfcGVyaW9kOiAxMHMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDMK",
|
||||
"compose": "c2VydmljZXM6CiAgZXNwb2NybToKICAgIGltYWdlOiAnZXNwb2NybS9lc3BvY3JtOjEwJwogICAgY29udGFpbmVyX25hbWU6IGVzcG9jcm0KICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX0VTUE9DUk0KICAgICAgLSAnRVNQT0NSTV9BRE1JTl9VU0VSTkFNRT0ke0VTUE9DUk1fQURNSU5fVVNFUk5BTUU6LWFkbWlufScKICAgICAgLSAnRVNQT0NSTV9BRE1JTl9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQURNSU59JwogICAgICAtIEVTUE9DUk1fREFUQUJBU0VfUExBVEZPUk09TXlzcWwKICAgICAgLSBFU1BPQ1JNX0RBVEFCQVNFX0hPU1Q9ZXNwb2NybS1kYgogICAgICAtICdFU1BPQ1JNX0RBVEFCQVNFX05BTUU9JHtNQVJJQURCX0RBVEFCQVNFOi1lc3BvY3JtfScKICAgICAgLSAnRVNQT0NSTV9EQVRBQkFTRV9VU0VSPSR7U0VSVklDRV9VU0VSX01BUklBREJ9JwogICAgICAtICdFU1BPQ1JNX0RBVEFCQVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9NQVJJQURCfScKICAgICAgLSAnRVNQT0NSTV9TSVRFX1VSTD0ke1NFUlZJQ0VfVVJMX0VTUE9DUk19JwogICAgdm9sdW1lczoKICAgICAgLSAnZXNwb2NybS1kYXRhOi92YXIvd3d3L2h0bWwvZGF0YScKICAgICAgLSAnZXNwb2NybS1jdXN0b206L3Zhci93d3cvaHRtbC9jdXN0b20nCiAgICAgIC0gJ2VzcG9jcm0tY3VzdG9tLWNsaWVudDovdmFyL3d3dy9odG1sL2NsaWVudC9jdXN0b20nCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgZGVwZW5kc19vbjoKICAgICAgZXNwb2NybS1kYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGJpbi9jb21tYW5kCiAgICAgICAgLSBhcHAtY2hlY2sKICAgICAgc3RhcnRfcGVyaW9kOiAyMHMKICAgICAgaW50ZXJ2YWw6IDYwcwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMwogIGVzcG9jcm0tZGFlbW9uOgogICAgaW1hZ2U6ICdlc3BvY3JtL2VzcG9jcm06MTAnCiAgICBjb250YWluZXJfbmFtZTogZXNwb2NybS1kYWVtb24KICAgIHZvbHVtZXNfZnJvbToKICAgICAgLSBlc3BvY3JtCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgZW50cnlwb2ludDogZG9ja2VyLWRhZW1vbi5zaAogICAgZGVwZW5kc19vbjoKICAgICAgZXNwb2NybToKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGJpbi9jb21tYW5kCiAgICAgICAgLSBhcHAtY2hlY2sKICAgICAgc3RhcnRfcGVyaW9kOiAyMHMKICAgICAgaW50ZXJ2YWw6IDE4MHMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDMKICBlc3BvY3JtLXdlYnNvY2tldDoKICAgIGltYWdlOiAnZXNwb2NybS9lc3BvY3JtOjEwJwogICAgY29udGFpbmVyX25hbWU6IGVzcG9jcm0td2Vic29ja2V0CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9FU1BPQ1JNX1dFQlNPQ0tFVF84MDgwCiAgICAgIC0gRVNQT0NSTV9DT05GSUdfVVNFX1dFQl9TT0NLRVQ9dHJ1ZQogICAgICAtIEVTUE9DUk1fQ09ORklHX1dFQl9TT0NLRVRfVVJMPSRTRVJWSUNFX1VSTF9FU1BPQ1JNX1dFQlNPQ0tFVAogICAgICAtICdFU1BPQ1JNX0NPTkZJR19XRUJfU09DS0VUX1pFUk9fTV9RX1NVQlNDUklCRVJfRFNOPXRjcDovLyo6Nzc3NycKICAgICAgLSAnRVNQT0NSTV9DT05GSUdfV0VCX1NPQ0tFVF9aRVJPX01fUV9TVUJNSVNTSU9OX0RTTj10Y3A6Ly9lc3BvY3JtLXdlYnNvY2tldDo3Nzc3JwogICAgdm9sdW1lc19mcm9tOgogICAgICAtIGVzcG9jcm0KICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBlbnRyeXBvaW50OiBkb2NrZXItd2Vic29ja2V0LnNoCiAgICBkZXBlbmRzX29uOgogICAgICBlc3BvY3JtOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gYmluL2NvbW1hbmQKICAgICAgICAtIGFwcC1jaGVjawogICAgICBzdGFydF9wZXJpb2Q6IDIwcwogICAgICBpbnRlcnZhbDogMTgwcwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMwogIGVzcG9jcm0tZGI6CiAgICBpbWFnZTogJ21hcmlhZGI6MTIuMycKICAgIGNvbnRhaW5lcl9uYW1lOiBlc3BvY3JtLWRiCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTUFSSUFEQl9EQVRBQkFTRT0ke01BUklBREJfREFUQUJBU0U6LWVzcG9jcm19JwogICAgICAtICdNQVJJQURCX1VTRVI9JHtTRVJWSUNFX1VTRVJfTUFSSUFEQn0nCiAgICAgIC0gJ01BUklBREJfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01BUklBREJ9JwogICAgICAtICdNQVJJQURCX1JPT1RfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1JPT1R9JwogICAgdm9sdW1lczoKICAgICAgLSAnZXNwb2NybS1kYjovdmFyL2xpYi9teXNxbCcKICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gaGVhbHRoY2hlY2suc2gKICAgICAgICAtICctLWNvbm5lY3QnCiAgICAgICAgLSAnLS1pbm5vZGJfaW5pdGlhbGl6ZWQnCiAgICAgIGludGVydmFsOiAyMHMKICAgICAgc3RhcnRfcGVyaW9kOiAxMHMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDMK",
|
||||
"tags": [
|
||||
"crm",
|
||||
"self-hosted",
|
||||
|
|
@ -1322,7 +1322,7 @@
|
|||
"category": "productivity",
|
||||
"logo": "svgs/espocrm.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2026-04-06T11:35:16-05:00",
|
||||
"template_last_updated_at": "2026-07-03T15:15:43+03:00",
|
||||
"port": "80"
|
||||
},
|
||||
"evolution-api": {
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@
|
|||
"alexandrie": {
|
||||
"documentation": "https://github.com/Smaug6739/Alexandrie/tree/main/docs?utm_source=coolify.io",
|
||||
"slogan": "A powerful Markdown workspace designed for speed, clarity, and creativity.",
|
||||
"compose": "c2VydmljZXM6CiAgZnJvbnRlbmQ6CiAgICBpbWFnZTogJ2doY3IuaW8vc21hdWc2NzM5L2FsZXhhbmRyaWUtZnJvbnRlbmQ6djguNy4yJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0ZST05URU5EXzgyMDAKICAgICAgLSBQT1JUPTgyMDAKICAgICAgLSAnTlVYVF9QVUJMSUNfQ09ORklHX0RJU0FCTEVfU0lHTlVQX1BBR0U9JHtDT05GSUdfRElTQUJMRV9TSUdOVVA6LWZhbHNlfScKICAgICAgLSAnTlVYVF9QVUJMSUNfQ09ORklHX0RJU0FCTEVfTEFORElOR19QQUdFPSR7Q09ORklHX0RJU0FCTEVfTEFORElORzotZmFsc2V9JwogICAgICAtICdOVVhUX1BVQkxJQ19CQVNFX0FQST0ke1NFUlZJQ0VfRlFETl9CQUNLRU5EfScKICAgICAgLSAnTlVYVF9QVUJMSUNfQkFTRV9DRE49JHtTRVJWSUNFX0ZRRE5fUlVTVEZTfScKICAgICAgLSAnTlVYVF9QVUJMSUNfQ0ROX0VORFBPSU5UPSR7Q0ROX0VORFBPSU5UOi0vYWxleGFuZHJpZS99JwogICAgICAtICdOVVhUX1BVQkxJQ19CQVNFX1VSTD0ke1NFUlZJQ0VfRlFETl9GUk9OVEVORH0nCiAgICBkZXBlbmRzX29uOgogICAgICAtIGJhY2tlbmQKICBiYWNrZW5kOgogICAgaW1hZ2U6ICdnaGNyLmlvL3NtYXVnNjczOS9hbGV4YW5kcmllLWJhY2tlbmQ6djguNy4yJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0JBQ0tFTkRfODIwMQogICAgICAtIEJBQ0tFTkRfUE9SVD04MjAxCiAgICAgIC0gR0lOX01PREU9cmVsZWFzZQogICAgICAtICdKV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1R9JwogICAgICAtICdDT09LSUVfRE9NQUlOPSR7U0VSVklDRV9GUUROX0ZST05URU5EfScKICAgICAgLSAnRlJPTlRFTkRfVVJMPSR7U0VSVklDRV9GUUROX0ZST05URU5EfScKICAgICAgLSAnQUxMT1dfVU5TRUNVUkU9JHtBTExPV19VTlNFQ1VSRTotZmFsc2V9JwogICAgICAtIERBVEFCQVNFX0hPU1Q9bXlzcWwKICAgICAgLSBEQVRBQkFTRV9QT1JUPTMzMDYKICAgICAgLSAnREFUQUJBU0VfTkFNRT0ke01ZU1FMX0RBVEFCQVNFOi1hbGV4YW5kcmllLWRifScKICAgICAgLSAnREFUQUJBU0VfVVNFUj0ke1NFUlZJQ0VfVVNFUl9NWVNRTH0nCiAgICAgIC0gJ0RBVEFCQVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9NWVNRTH0nCiAgICAgIC0gJ01JTklPX0VORFBPSU5UPXJ1c3Rmczo5MDAwJwogICAgICAtICdNSU5JT19QVUJMSUNfVVJMPSR7U0VSVklDRV9GUUROX1JVU1RGU30nCiAgICAgIC0gJ01JTklPX1NFQ1VSRT0ke01JTklPX1NFQ1VSRTotZmFsc2V9JwogICAgICAtICdNSU5JT19BQ0NFU1NLRVk9JHtTRVJWSUNFX1VTRVJfUlVTVEZTfScKICAgICAgLSAnTUlOSU9fU0VDUkVUS0VZPSR7U0VSVklDRV9QQVNTV09SRF9SVVNURlN9JwogICAgICAtICdNSU5JT19CVUNLRVQ9JHtNSU5JT19CVUNLRVQ6LWFsZXhhbmRyaWV9JwogICAgICAtICdTTVRQX0hPU1Q9JHtTTVRQX0hPU1Q6LX0nCiAgICAgIC0gJ1NNVFBfTUFJTD0ke1NNVFBfTUFJTDotfScKICAgICAgLSAnU01UUF9QQVNTV09SRD0ke1NNVFBfUEFTU1dPUkQ6LX0nCiAgICBkZXBlbmRzX29uOgogICAgICBteXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBydXN0ZnM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICBteXNxbDoKICAgIGltYWdlOiAnbXlzcWw6OC4wJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ01ZU1FMX1JPT1RfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01ZU1FMUk9PVH0nCiAgICAgIC0gJ01ZU1FMX1VTRVI9JHtTRVJWSUNFX1VTRVJfTVlTUUx9JwogICAgICAtICdNWVNRTF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUx9JwogICAgICAtICdNWVNRTF9EQVRBQkFTRT0ke01ZU1FMX0RBVEFCQVNFOi1hbGV4YW5kcmllLWRifScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ215c3FsLWRhdGE6L3Zhci9saWIvbXlzcWwnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gbXlzcWxhZG1pbgogICAgICAgIC0gcGluZwogICAgICAgIC0gJy1oJwogICAgICAgIC0gbG9jYWxob3N0CiAgICAgICAgLSAnLXUnCiAgICAgICAgLSByb290CiAgICAgICAgLSAnLXAke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUxST09UfScKICAgICAgdGltZW91dDogNXMKICAgICAgaW50ZXJ2YWw6IDEwcwogICAgICByZXRyaWVzOiA1CiAgcnVzdGZzOgogICAgaW1hZ2U6ICdydXN0ZnMvcnVzdGZzOjEuMC4wLWFscGhhLjkwJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX1JVU1RGU185MDAwCiAgICAgIC0gJ1JVU1RGU19BQ0NFU1NfS0VZPSR7U0VSVklDRV9VU0VSX1JVU1RGU30nCiAgICAgIC0gJ1JVU1RGU19TRUNSRVRfS0VZPSR7U0VSVklDRV9QQVNTV09SRF9SVVNURlN9JwogICAgICAtICdSVVNURlNfQ09OU09MRV9FTkFCTEU9JHtSVVNURlNfQ09OU09MRV9FTkFCTEU6LWZhbHNlfScKICAgICAgLSAnUlVTVEZTX0xPR19MRVZFTD0ke1JVU1RGU19MT0dfTEVWRUw6LWluZm99JwogICAgdm9sdW1lczoKICAgICAgLSAncnVzdGZzLWRhdGE6L2RhdGEnCiAgICAgIC0gJ3J1c3Rmcy1sb2dzOi9sb2dzJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICduYyAteiBsb2NhbGhvc3QgOTAwMCB8fCBleGl0IDEnCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogNQo=",
|
||||
"compose": "c2VydmljZXM6CiAgZnJvbnRlbmQ6CiAgICBpbWFnZTogJ2doY3IuaW8vc21hdWc2NzM5L2FsZXhhbmRyaWUtZnJvbnRlbmQ6djguMTAuMCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9GUk9OVEVORF84MjAwCiAgICAgIC0gUE9SVD04MjAwCiAgICAgIC0gJ05VWFRfUFVCTElDX0NPTkZJR19ESVNBQkxFX1NJR05VUF9QQUdFPSR7Q09ORklHX0RJU0FCTEVfU0lHTlVQOi1mYWxzZX0nCiAgICAgIC0gJ05VWFRfUFVCTElDX0NPTkZJR19ESVNBQkxFX0xBTkRJTkdfUEFHRT0ke0NPTkZJR19ESVNBQkxFX0xBTkRJTkc6LWZhbHNlfScKICAgICAgLSAnTlVYVF9QVUJMSUNfQkFTRV9BUEk9JHtTRVJWSUNFX0ZRRE5fQkFDS0VORH0nCiAgICAgIC0gJ05VWFRfUFVCTElDX0JBU0VfQ0ROPSR7U0VSVklDRV9GUUROX1JVU1RGU30nCiAgICAgIC0gJ05VWFRfUFVCTElDX0NETl9FTkRQT0lOVD0ke0NETl9FTkRQT0lOVDotL2FsZXhhbmRyaWUvfScKICAgICAgLSAnTlVYVF9QVUJMSUNfQkFTRV9VUkw9JHtTRVJWSUNFX0ZRRE5fRlJPTlRFTkR9JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBiYWNrZW5kCiAgYmFja2VuZDoKICAgIGltYWdlOiAnZ2hjci5pby9zbWF1ZzY3MzkvYWxleGFuZHJpZS1iYWNrZW5kOnY4LjEwLjAnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fQkFDS0VORF84MjAxCiAgICAgIC0gQkFDS0VORF9QT1JUPTgyMDEKICAgICAgLSBHSU5fTU9ERT1yZWxlYXNlCiAgICAgIC0gJ0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVH0nCiAgICAgIC0gJ0NPT0tJRV9ET01BSU49JHtTRVJWSUNFX0ZRRE5fRlJPTlRFTkR9JwogICAgICAtICdGUk9OVEVORF9VUkw9JHtTRVJWSUNFX0ZRRE5fRlJPTlRFTkR9JwogICAgICAtICdBTExPV19VTlNFQ1VSRT0ke0FMTE9XX1VOU0VDVVJFOi1mYWxzZX0nCiAgICAgIC0gREFUQUJBU0VfSE9TVD1teXNxbAogICAgICAtIERBVEFCQVNFX1BPUlQ9MzMwNgogICAgICAtICdEQVRBQkFTRV9OQU1FPSR7TVlTUUxfREFUQUJBU0U6LWFsZXhhbmRyaWUtZGJ9JwogICAgICAtICdEQVRBQkFTRV9VU0VSPSR7U0VSVklDRV9VU0VSX01ZU1FMfScKICAgICAgLSAnREFUQUJBU0VfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01ZU1FMfScKICAgICAgLSAnTUlOSU9fRU5EUE9JTlQ9cnVzdGZzOjkwMDAnCiAgICAgIC0gJ01JTklPX1BVQkxJQ19VUkw9JHtTRVJWSUNFX0ZRRE5fUlVTVEZTfScKICAgICAgLSAnTUlOSU9fU0VDVVJFPSR7TUlOSU9fU0VDVVJFOi1mYWxzZX0nCiAgICAgIC0gJ01JTklPX0FDQ0VTU0tFWT0ke1NFUlZJQ0VfVVNFUl9SVVNURlN9JwogICAgICAtICdNSU5JT19TRUNSRVRLRVk9JHtTRVJWSUNFX1BBU1NXT1JEX1JVU1RGU30nCiAgICAgIC0gJ01JTklPX0JVQ0tFVD0ke01JTklPX0JVQ0tFVDotYWxleGFuZHJpZX0nCiAgICAgIC0gJ1NNVFBfSE9TVD0ke1NNVFBfSE9TVDotfScKICAgICAgLSAnU01UUF9NQUlMPSR7U01UUF9NQUlMOi19JwogICAgICAtICdTTVRQX1BBU1NXT1JEPSR7U01UUF9QQVNTV09SRDotfScKICAgIGRlcGVuZHNfb246CiAgICAgIG15c3FsOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJ1c3RmczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogIG15c3FsOgogICAgaW1hZ2U6ICdteXNxbDo4LjAnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTVlTUUxfUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUxST09UfScKICAgICAgLSAnTVlTUUxfVVNFUj0ke1NFUlZJQ0VfVVNFUl9NWVNRTH0nCiAgICAgIC0gJ01ZU1FMX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9NWVNRTH0nCiAgICAgIC0gJ01ZU1FMX0RBVEFCQVNFPSR7TVlTUUxfREFUQUJBU0U6LWFsZXhhbmRyaWUtZGJ9JwogICAgdm9sdW1lczoKICAgICAgLSAnbXlzcWwtZGF0YTovdmFyL2xpYi9teXNxbCcKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBteXNxbGFkbWluCiAgICAgICAgLSBwaW5nCiAgICAgICAgLSAnLWgnCiAgICAgICAgLSBsb2NhbGhvc3QKICAgICAgICAtICctdScKICAgICAgICAtIHJvb3QKICAgICAgICAtICctcCR7U0VSVklDRV9QQVNTV09SRF9NWVNRTFJPT1R9JwogICAgICB0aW1lb3V0OiA1cwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHJldHJpZXM6IDUKICBydXN0ZnM6CiAgICBpbWFnZTogJ3J1c3Rmcy9ydXN0ZnM6MS4wLjAtYmV0YS44JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX1JVU1RGU185MDAwCiAgICAgIC0gJ1JVU1RGU19BQ0NFU1NfS0VZPSR7U0VSVklDRV9VU0VSX1JVU1RGU30nCiAgICAgIC0gJ1JVU1RGU19TRUNSRVRfS0VZPSR7U0VSVklDRV9QQVNTV09SRF9SVVNURlN9JwogICAgICAtICdSVVNURlNfQ09OU09MRV9FTkFCTEU9JHtSVVNURlNfQ09OU09MRV9FTkFCTEU6LWZhbHNlfScKICAgICAgLSAnUlVTVEZTX0xPR19MRVZFTD0ke1JVU1RGU19MT0dfTEVWRUw6LWluZm99JwogICAgdm9sdW1lczoKICAgICAgLSAncnVzdGZzLWRhdGE6L2RhdGEnCiAgICAgIC0gJ3J1c3Rmcy1sb2dzOi9sb2dzJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICduYyAteiBsb2NhbGhvc3QgOTAwMCB8fCBleGl0IDEnCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogNQo=",
|
||||
"tags": [
|
||||
"note-taking",
|
||||
"markdown",
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
"category": "productivity",
|
||||
"logo": "svgs/alexandrie.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2026-04-05T13:36:24+02:00",
|
||||
"template_last_updated_at": "2026-07-07T13:24:35+02:00",
|
||||
"port": "8200"
|
||||
},
|
||||
"anythingllm": {
|
||||
|
|
@ -1310,7 +1310,7 @@
|
|||
"espocrm": {
|
||||
"documentation": "https://docs.espocrm.com?utm_source=coolify.io",
|
||||
"slogan": "EspoCRM is a free and open-source CRM platform.",
|
||||
"compose": "c2VydmljZXM6CiAgZXNwb2NybToKICAgIGltYWdlOiAnZXNwb2NybS9lc3BvY3JtOjknCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fRVNQT0NSTQogICAgICAtICdFU1BPQ1JNX0FETUlOX1VTRVJOQU1FPSR7RVNQT0NSTV9BRE1JTl9VU0VSTkFNRTotYWRtaW59JwogICAgICAtICdFU1BPQ1JNX0FETUlOX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9BRE1JTn0nCiAgICAgIC0gRVNQT0NSTV9EQVRBQkFTRV9QTEFURk9STT1NeXNxbAogICAgICAtIEVTUE9DUk1fREFUQUJBU0VfSE9TVD1lc3BvY3JtLWRiCiAgICAgIC0gJ0VTUE9DUk1fREFUQUJBU0VfTkFNRT0ke01BUklBREJfREFUQUJBU0U6LWVzcG9jcm19JwogICAgICAtICdFU1BPQ1JNX0RBVEFCQVNFX1VTRVI9JHtTRVJWSUNFX1VTRVJfTUFSSUFEQn0nCiAgICAgIC0gJ0VTUE9DUk1fREFUQUJBU0VfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01BUklBREJ9JwogICAgICAtICdFU1BPQ1JNX1NJVEVfVVJMPSR7U0VSVklDRV9GUUROX0VTUE9DUk19JwogICAgdm9sdW1lczoKICAgICAgLSAnZXNwb2NybTovdmFyL3d3dy9odG1sJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjgwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgc3RhcnRfcGVyaW9kOiA2MHMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgICBkZXBlbmRzX29uOgogICAgICBlc3BvY3JtLWRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgZXNwb2NybS1kYWVtb246CiAgICBpbWFnZTogJ2VzcG9jcm0vZXNwb2NybTo5JwogICAgY29udGFpbmVyX25hbWU6IGVzcG9jcm0tZGFlbW9uCiAgICB2b2x1bWVzOgogICAgICAtICdlc3BvY3JtOi92YXIvd3d3L2h0bWwnCiAgICByZXN0YXJ0OiBhbHdheXMKICAgIGVudHJ5cG9pbnQ6IGRvY2tlci1kYWVtb24uc2gKICAgIGRlcGVuZHNfb246CiAgICAgIGVzcG9jcm06CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICBlc3BvY3JtLXdlYnNvY2tldDoKICAgIGltYWdlOiAnZXNwb2NybS9lc3BvY3JtOjknCiAgICBjb250YWluZXJfbmFtZTogZXNwb2NybS13ZWJzb2NrZXQKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9FU1BPQ1JNX1dFQlNPQ0tFVF84MDgwCiAgICAgIC0gRVNQT0NSTV9DT05GSUdfVVNFX1dFQl9TT0NLRVQ9dHJ1ZQogICAgICAtIEVTUE9DUk1fQ09ORklHX1dFQl9TT0NLRVRfVVJMPSRTRVJWSUNFX0ZRRE5fRVNQT0NSTV9XRUJTT0NLRVQKICAgICAgLSAnRVNQT0NSTV9DT05GSUdfV0VCX1NPQ0tFVF9aRVJPX01fUV9TVUJTQ1JJQkVSX0RTTj10Y3A6Ly8qOjc3NzcnCiAgICAgIC0gJ0VTUE9DUk1fQ09ORklHX1dFQl9TT0NLRVRfWkVST19NX1FfU1VCTUlTU0lPTl9EU049dGNwOi8vZXNwb2NybS13ZWJzb2NrZXQ6Nzc3NycKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2VzcG9jcm06L3Zhci93d3cvaHRtbCcKICAgIHJlc3RhcnQ6IGFsd2F5cwogICAgZW50cnlwb2ludDogZG9ja2VyLXdlYnNvY2tldC5zaAogICAgZGVwZW5kc19vbjoKICAgICAgZXNwb2NybToKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogIGVzcG9jcm0tZGI6CiAgICBpbWFnZTogJ21hcmlhZGI6MTEuOCcKICAgIGVudmlyb25tZW50OgogICAgICAtICdNQVJJQURCX0RBVEFCQVNFPSR7TUFSSUFEQl9EQVRBQkFTRTotZXNwb2NybX0nCiAgICAgIC0gJ01BUklBREJfVVNFUj0ke1NFUlZJQ0VfVVNFUl9NQVJJQURCfScKICAgICAgLSAnTUFSSUFEQl9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQn0nCiAgICAgIC0gJ01BUklBREJfUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUk9PVH0nCiAgICB2b2x1bWVzOgogICAgICAtICdlc3BvY3JtLWRiOi92YXIvbGliL215c3FsJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGhlYWx0aGNoZWNrLnNoCiAgICAgICAgLSAnLS1jb25uZWN0JwogICAgICAgIC0gJy0taW5ub2RiX2luaXRpYWxpemVkJwogICAgICBpbnRlcnZhbDogMjBzCiAgICAgIHN0YXJ0X3BlcmlvZDogMTBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCg==",
|
||||
"compose": "c2VydmljZXM6CiAgZXNwb2NybToKICAgIGltYWdlOiAnZXNwb2NybS9lc3BvY3JtOjEwJwogICAgY29udGFpbmVyX25hbWU6IGVzcG9jcm0KICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9FU1BPQ1JNCiAgICAgIC0gJ0VTUE9DUk1fQURNSU5fVVNFUk5BTUU9JHtFU1BPQ1JNX0FETUlOX1VTRVJOQU1FOi1hZG1pbn0nCiAgICAgIC0gJ0VTUE9DUk1fQURNSU5fUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0FETUlOfScKICAgICAgLSBFU1BPQ1JNX0RBVEFCQVNFX1BMQVRGT1JNPU15c3FsCiAgICAgIC0gRVNQT0NSTV9EQVRBQkFTRV9IT1NUPWVzcG9jcm0tZGIKICAgICAgLSAnRVNQT0NSTV9EQVRBQkFTRV9OQU1FPSR7TUFSSUFEQl9EQVRBQkFTRTotZXNwb2NybX0nCiAgICAgIC0gJ0VTUE9DUk1fREFUQUJBU0VfVVNFUj0ke1NFUlZJQ0VfVVNFUl9NQVJJQURCfScKICAgICAgLSAnRVNQT0NSTV9EQVRBQkFTRV9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQn0nCiAgICAgIC0gJ0VTUE9DUk1fU0lURV9VUkw9JHtTRVJWSUNFX0ZRRE5fRVNQT0NSTX0nCiAgICB2b2x1bWVzOgogICAgICAtICdlc3BvY3JtLWRhdGE6L3Zhci93d3cvaHRtbC9kYXRhJwogICAgICAtICdlc3BvY3JtLWN1c3RvbTovdmFyL3d3dy9odG1sL2N1c3RvbScKICAgICAgLSAnZXNwb2NybS1jdXN0b20tY2xpZW50Oi92YXIvd3d3L2h0bWwvY2xpZW50L2N1c3RvbScKICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBkZXBlbmRzX29uOgogICAgICBlc3BvY3JtLWRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gYmluL2NvbW1hbmQKICAgICAgICAtIGFwcC1jaGVjawogICAgICBzdGFydF9wZXJpb2Q6IDIwcwogICAgICBpbnRlcnZhbDogNjBzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAzCiAgZXNwb2NybS1kYWVtb246CiAgICBpbWFnZTogJ2VzcG9jcm0vZXNwb2NybToxMCcKICAgIGNvbnRhaW5lcl9uYW1lOiBlc3BvY3JtLWRhZW1vbgogICAgdm9sdW1lc19mcm9tOgogICAgICAtIGVzcG9jcm0KICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBlbnRyeXBvaW50OiBkb2NrZXItZGFlbW9uLnNoCiAgICBkZXBlbmRzX29uOgogICAgICBlc3BvY3JtOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gYmluL2NvbW1hbmQKICAgICAgICAtIGFwcC1jaGVjawogICAgICBzdGFydF9wZXJpb2Q6IDIwcwogICAgICBpbnRlcnZhbDogMTgwcwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMwogIGVzcG9jcm0td2Vic29ja2V0OgogICAgaW1hZ2U6ICdlc3BvY3JtL2VzcG9jcm06MTAnCiAgICBjb250YWluZXJfbmFtZTogZXNwb2NybS13ZWJzb2NrZXQKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9FU1BPQ1JNX1dFQlNPQ0tFVF84MDgwCiAgICAgIC0gRVNQT0NSTV9DT05GSUdfVVNFX1dFQl9TT0NLRVQ9dHJ1ZQogICAgICAtIEVTUE9DUk1fQ09ORklHX1dFQl9TT0NLRVRfVVJMPSRTRVJWSUNFX0ZRRE5fRVNQT0NSTV9XRUJTT0NLRVQKICAgICAgLSAnRVNQT0NSTV9DT05GSUdfV0VCX1NPQ0tFVF9aRVJPX01fUV9TVUJTQ1JJQkVSX0RTTj10Y3A6Ly8qOjc3NzcnCiAgICAgIC0gJ0VTUE9DUk1fQ09ORklHX1dFQl9TT0NLRVRfWkVST19NX1FfU1VCTUlTU0lPTl9EU049dGNwOi8vZXNwb2NybS13ZWJzb2NrZXQ6Nzc3NycKICAgIHZvbHVtZXNfZnJvbToKICAgICAgLSBlc3BvY3JtCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgZW50cnlwb2ludDogZG9ja2VyLXdlYnNvY2tldC5zaAogICAgZGVwZW5kc19vbjoKICAgICAgZXNwb2NybToKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGJpbi9jb21tYW5kCiAgICAgICAgLSBhcHAtY2hlY2sKICAgICAgc3RhcnRfcGVyaW9kOiAyMHMKICAgICAgaW50ZXJ2YWw6IDE4MHMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDMKICBlc3BvY3JtLWRiOgogICAgaW1hZ2U6ICdtYXJpYWRiOjEyLjMnCiAgICBjb250YWluZXJfbmFtZTogZXNwb2NybS1kYgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ01BUklBREJfREFUQUJBU0U9JHtNQVJJQURCX0RBVEFCQVNFOi1lc3BvY3JtfScKICAgICAgLSAnTUFSSUFEQl9VU0VSPSR7U0VSVklDRV9VU0VSX01BUklBREJ9JwogICAgICAtICdNQVJJQURCX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9NQVJJQURCfScKICAgICAgLSAnTUFSSUFEQl9ST09UX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9ST09UfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2VzcG9jcm0tZGI6L3Zhci9saWIvbXlzcWwnCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGhlYWx0aGNoZWNrLnNoCiAgICAgICAgLSAnLS1jb25uZWN0JwogICAgICAgIC0gJy0taW5ub2RiX2luaXRpYWxpemVkJwogICAgICBpbnRlcnZhbDogMjBzCiAgICAgIHN0YXJ0X3BlcmlvZDogMTBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCg==",
|
||||
"tags": [
|
||||
"crm",
|
||||
"self-hosted",
|
||||
|
|
@ -1322,7 +1322,7 @@
|
|||
"category": "productivity",
|
||||
"logo": "svgs/espocrm.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2026-04-06T11:35:16-05:00",
|
||||
"template_last_updated_at": "2026-07-03T15:15:43+03:00",
|
||||
"port": "80"
|
||||
},
|
||||
"evolution-api": {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Livewire\Features\SupportLockedProperties\CannotUpdateLockedPropertyException;
|
||||
use Livewire\Livewire;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
|
@ -117,6 +118,48 @@ function makePrivateKey(string $name, string $material, string $fingerprint, int
|
|||
expect($this->applicationA->source_type)->not->toBe(Server::class);
|
||||
});
|
||||
|
||||
test('changeSource dispatches configuration changed for an owned source', function () {
|
||||
Http::fake([
|
||||
'https://api.github.com/repos/*' => Http::response(['id' => 123]),
|
||||
]);
|
||||
$source = GithubApp::create([
|
||||
'name' => 'own-github-app',
|
||||
'team_id' => $this->teamA->id,
|
||||
'api_url' => 'https://api.github.com',
|
||||
'html_url' => 'https://github.com',
|
||||
'is_public' => true,
|
||||
]);
|
||||
$this->applicationA->update(['git_repository' => 'coollabsio/coolify']);
|
||||
|
||||
Livewire::test(Source::class, ['application' => $this->applicationA->fresh()])
|
||||
->call('changeSource', $source->id, GithubApp::class)
|
||||
->assertDispatched('configurationChanged');
|
||||
});
|
||||
|
||||
test('changeSource dispatches configuration changed when repository metadata lookup fails after persistence', function () {
|
||||
Http::fake([
|
||||
'https://api.github.com/repos/*' => Http::response(
|
||||
['message' => 'Unavailable'],
|
||||
503,
|
||||
['X-RateLimit-Reset' => now()->addMinute()->timestamp],
|
||||
),
|
||||
]);
|
||||
$source = GithubApp::create([
|
||||
'name' => 'own-unavailable-github-app',
|
||||
'team_id' => $this->teamA->id,
|
||||
'api_url' => 'https://api.github.com',
|
||||
'html_url' => 'https://github.com',
|
||||
'is_public' => true,
|
||||
]);
|
||||
$this->applicationA->update(['git_repository' => 'coollabsio/coolify']);
|
||||
|
||||
Livewire::test(Source::class, ['application' => $this->applicationA->fresh()])
|
||||
->call('changeSource', $source->id, GithubApp::class)
|
||||
->assertDispatched('configurationChanged');
|
||||
|
||||
expect($this->applicationA->refresh()->source_id)->toBe($source->id);
|
||||
});
|
||||
|
||||
test('privateKeyId is locked so submit() cannot persist a client-supplied foreign id', function () {
|
||||
// Without #[Locked], an attacker could POST {"updates": {"privateKeyId": <foreign_id>},
|
||||
// "calls": [{"method": "submit"}]} and have syncData(true) write the foreign id through
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@
|
|||
function createApplicationForAdvancedStopGracePeriodTest(): Application
|
||||
{
|
||||
$team = Team::factory()->create();
|
||||
$team->members()->attach(auth()->id(), ['role' => 'owner']);
|
||||
session(['currentTeam' => $team]);
|
||||
$server = Server::factory()->create(['team_id' => $team->id]);
|
||||
$project = Project::factory()->create(['team_id' => $team->id]);
|
||||
$environment = Environment::factory()->create(['project_id' => $project->id]);
|
||||
|
|
@ -43,7 +45,8 @@ function createApplicationForAdvancedStopGracePeriodTest(): Application
|
|||
->set('stopGracePeriod', '300')
|
||||
->call('saveStopGracePeriod')
|
||||
->assertHasNoErrors()
|
||||
->assertDispatched('success');
|
||||
->assertDispatched('success')
|
||||
->assertDispatched('configurationChanged');
|
||||
|
||||
expect($application->settings()->first()->stop_grace_period)->toBe(300);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
use App\Livewire\Project\Shared\EnvironmentVariable\All as EnvironmentVariableAll;
|
||||
use App\Livewire\Project\Shared\HealthChecks;
|
||||
use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Project;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
InstanceSettings::unguarded(function () {
|
||||
InstanceSettings::updateOrCreate(['id' => 0], []);
|
||||
});
|
||||
$this->team = Team::factory()->create();
|
||||
$this->user = User::factory()->create();
|
||||
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
|
||||
$this->actingAs($this->user);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$project = Project::factory()->create(['team_id' => $this->team->id]);
|
||||
$environment = Environment::factory()->create(['project_id' => $project->id]);
|
||||
$this->application = Application::factory()->create(['environment_id' => $environment->id])->refresh();
|
||||
});
|
||||
|
||||
it('refreshes configuration changes after health check saves', function (string $method) {
|
||||
Livewire::test(HealthChecks::class, ['resource' => $this->application])
|
||||
->call($method)
|
||||
->assertHasNoErrors()
|
||||
->assertDispatched('configurationChanged');
|
||||
})->with(['instantSave', 'submit', 'toggleHealthcheck']);
|
||||
|
||||
it('refreshes configuration changes after environment variable settings are saved', function () {
|
||||
Livewire::test(EnvironmentVariableAll::class, ['resource' => $this->application])
|
||||
->set('use_build_secrets', true)
|
||||
->call('instantSave')
|
||||
->assertHasNoErrors()
|
||||
->assertDispatched('configurationChanged');
|
||||
});
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
use App\Livewire\Project\Application\Swarm;
|
||||
use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('dispatches configuration changed when Swarm settings are saved', function (string $method) {
|
||||
$team = Team::factory()->create();
|
||||
$user = User::factory()->create();
|
||||
$team->members()->attach($user->id, ['role' => 'owner']);
|
||||
$server = Server::factory()->create(['team_id' => $team->id]);
|
||||
$project = Project::factory()->create(['team_id' => $team->id]);
|
||||
$environment = Environment::factory()->create(['project_id' => $project->id]);
|
||||
$application = Application::factory()->create([
|
||||
'environment_id' => $environment->id,
|
||||
'destination_id' => $server->standaloneDockers()->firstOrFail()->id,
|
||||
'destination_type' => $server->standaloneDockers()->firstOrFail()->getMorphClass(),
|
||||
'swarm_replicas' => 1,
|
||||
]);
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
Livewire::test(Swarm::class, ['application' => $application])
|
||||
->set('isSwarmOnlyWorkerNodes', false)
|
||||
->call($method)
|
||||
->assertHasNoErrors()
|
||||
->assertDispatched('configurationChanged');
|
||||
})->with(['instantSave', 'submit']);
|
||||
|
|
@ -118,4 +118,24 @@
|
|||
->assertSee('nixpacks.toml')
|
||||
->assertDontSee('railpack.json');
|
||||
});
|
||||
|
||||
test('saving general settings refreshes configuration changes when label reset is disabled', function () {
|
||||
$application = Application::factory()->create([
|
||||
'environment_id' => $this->environment->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => StandaloneDocker::class,
|
||||
'build_pack' => 'nixpacks',
|
||||
'static_image' => 'nginx:alpine',
|
||||
'base_directory' => '/',
|
||||
'is_http_basic_auth_enabled' => false,
|
||||
'redirect' => 'no',
|
||||
]);
|
||||
$application->settings->update(['is_container_label_readonly_enabled' => false]);
|
||||
|
||||
Livewire::test(General::class, ['application' => $application->refresh()])
|
||||
->set('isPreserveRepositoryEnabled', true)
|
||||
->call('instantSave')
|
||||
->assertHasNoErrors()
|
||||
->assertDispatched('configurationChanged');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
use App\Models\EnvironmentVariable;
|
||||
use App\Models\Project;
|
||||
use App\Models\Team;
|
||||
use App\Services\DeploymentConfiguration\ConfigurationDiffer;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Str;
|
||||
use Tests\TestCase;
|
||||
|
|
@ -78,6 +79,192 @@ function markSnapshotTestApplicationDeployed(Application $application): Applicat
|
|||
->and($change['new_full_value'])->toBe($domains);
|
||||
});
|
||||
|
||||
it('detects Docker image reference changes as redeploy-only changes', function (string $field, string $label, string $newValue) {
|
||||
$application = snapshotTestApplication([
|
||||
'build_pack' => 'dockerimage',
|
||||
'docker_registry_image_name' => 'ghcr.io/coollabsio/shoutrrr',
|
||||
'docker_registry_image_tag' => '1.3.0-rc.4',
|
||||
]);
|
||||
markSnapshotTestApplicationDeployed($application);
|
||||
|
||||
$application->update([$field => $newValue]);
|
||||
$diff = $application->refresh()->pendingDeploymentConfigurationDiff();
|
||||
$change = collect($diff->changes())->firstWhere('label', $label);
|
||||
|
||||
expect($diff->isChanged())->toBeTrue()
|
||||
->and($diff->requiresBuild())->toBeFalse()
|
||||
->and($change)->not->toBeNull()
|
||||
->and($change['old_display_value'])->not->toBe($change['new_display_value'])
|
||||
->and($change['new_display_value'])->toBe($newValue);
|
||||
})->with([
|
||||
'image name' => ['docker_registry_image_name', 'Docker image', 'ghcr.io/coollabsio/coolify'],
|
||||
'image tag' => ['docker_registry_image_tag', 'Docker image tag or hash', '1.3.0-rc.5'],
|
||||
]);
|
||||
|
||||
it('detects deployment hook changes as redeploy-only changes', function (string $field, string $label, string $newValue) {
|
||||
$application = snapshotTestApplication();
|
||||
markSnapshotTestApplicationDeployed($application);
|
||||
|
||||
$application->update([$field => $newValue]);
|
||||
$diff = $application->refresh()->pendingDeploymentConfigurationDiff();
|
||||
|
||||
expect($diff->requiresBuild())->toBeFalse()
|
||||
->and(collect($diff->changes())->pluck('label'))->toContain($label);
|
||||
})->with([
|
||||
'pre-deployment command' => ['pre_deployment_command', 'Pre-deployment command', 'php artisan migrate --force'],
|
||||
'pre-deployment container' => ['pre_deployment_command_container', 'Pre-deployment command container', 'web'],
|
||||
'post-deployment command' => ['post_deployment_command', 'Post-deployment command', 'php artisan cache:clear'],
|
||||
'post-deployment container' => ['post_deployment_command_container', 'Post-deployment command container', 'worker'],
|
||||
]);
|
||||
|
||||
it('detects source integration changes as build changes', function (string $field, string $label, mixed $newValue) {
|
||||
$application = snapshotTestApplication([
|
||||
'source_id' => 1,
|
||||
'source_type' => 'App\\Models\\GithubApp',
|
||||
]);
|
||||
markSnapshotTestApplicationDeployed($application);
|
||||
|
||||
$application->forceFill([$field => $newValue])->save();
|
||||
$diff = $application->refresh()->pendingDeploymentConfigurationDiff();
|
||||
|
||||
expect($diff->requiresBuild())->toBeTrue()
|
||||
->and(collect($diff->changes())->pluck('label'))->toContain($label);
|
||||
})->with([
|
||||
'source ID' => ['source_id', 'Source ID', 2],
|
||||
'source type' => ['source_type', 'Source type', 'App\\Models\\GitlabApp'],
|
||||
]);
|
||||
|
||||
it('detects build-affecting application settings', function (string $field, string $label) {
|
||||
$application = snapshotTestApplication();
|
||||
$application->settings->update([$field => false]);
|
||||
markSnapshotTestApplicationDeployed($application);
|
||||
|
||||
$application->settings->update([$field => true]);
|
||||
$diff = $application->refresh()->pendingDeploymentConfigurationDiff();
|
||||
|
||||
expect($diff->requiresBuild())->toBeTrue()
|
||||
->and(collect($diff->changes())->pluck('label'))->toContain($label);
|
||||
})->with([
|
||||
'static site' => ['is_static', 'Static site'],
|
||||
'single-page application' => ['is_spa', 'Single-page application'],
|
||||
'Git submodules' => ['is_git_submodules_enabled', 'Git submodules'],
|
||||
'Git LFS' => ['is_git_lfs_enabled', 'Git LFS'],
|
||||
'shallow clone' => ['is_git_shallow_clone_enabled', 'Shallow clone'],
|
||||
'environment variable sorting' => ['is_env_sorting_enabled', 'Sort environment variables'],
|
||||
]);
|
||||
|
||||
it('detects runtime-affecting application settings as redeploy-only changes', function (string $field, string $label, mixed $newValue) {
|
||||
$application = snapshotTestApplication();
|
||||
$application->settings->update([$field => is_bool($newValue) ? ! $newValue : null]);
|
||||
markSnapshotTestApplicationDeployed($application);
|
||||
|
||||
$application->settings->update([$field => $newValue]);
|
||||
$diff = $application->refresh()->pendingDeploymentConfigurationDiff();
|
||||
|
||||
expect($diff->requiresBuild())->toBeFalse()
|
||||
->and(collect($diff->changes())->pluck('label'))->toContain($label);
|
||||
})->with([
|
||||
'consistent container name' => ['is_consistent_container_name_enabled', 'Consistent container name', true],
|
||||
'container label escaping' => ['is_container_label_escape_enabled', 'Escape container labels', false],
|
||||
'container labels read-only' => ['is_container_label_readonly_enabled', 'Read-only container labels', false],
|
||||
'log drain' => ['is_log_drain_enabled', 'Log drain', true],
|
||||
'Swarm worker nodes' => ['is_swarm_only_worker_nodes', 'Swarm worker nodes only', true],
|
||||
'stop grace period' => ['stop_grace_period', 'Stop grace period', 45],
|
||||
'preserve repository' => ['is_preserve_repository_enabled', 'Preserve repository', true],
|
||||
]);
|
||||
|
||||
it('classifies runtime options as redeploy-only and custom nginx configuration as build-affecting', function () {
|
||||
$application = snapshotTestApplication();
|
||||
markSnapshotTestApplicationDeployed($application);
|
||||
|
||||
$application->update(['custom_docker_run_options' => '--init']);
|
||||
|
||||
expect($application->refresh()->pendingDeploymentConfigurationDiff()->requiresBuild())->toBeFalse();
|
||||
|
||||
markSnapshotTestApplicationDeployed($application->refresh());
|
||||
$application->update(['custom_nginx_configuration' => 'server { listen 80; }']);
|
||||
|
||||
expect($application->refresh()->pendingDeploymentConfigurationDiff()->requiresBuild())->toBeTrue();
|
||||
});
|
||||
|
||||
it('keeps Docker run options compatible with older build-section snapshots', function () {
|
||||
$application = snapshotTestApplication(['custom_docker_run_options' => '--init'])->refresh();
|
||||
$previousSnapshot = $application->deploymentConfigurationSnapshot();
|
||||
$buildItems = collect(data_get($previousSnapshot, 'sections.build.items'));
|
||||
|
||||
expect($buildItems->pluck('key'))->toContain('custom_docker_run_options');
|
||||
|
||||
data_set(
|
||||
$previousSnapshot,
|
||||
'sections.build.items',
|
||||
$buildItems->map(function (array $item): array {
|
||||
if ($item['key'] === 'custom_docker_run_options') {
|
||||
$item['impact'] = 'build';
|
||||
}
|
||||
|
||||
return $item;
|
||||
})->all(),
|
||||
);
|
||||
|
||||
expect(app(ConfigurationDiffer::class)->diff($previousSnapshot, $application->deploymentConfigurationSnapshot())->isChanged())->toBeFalse();
|
||||
|
||||
$application->update(['custom_docker_run_options' => '--rm']);
|
||||
|
||||
expect(app(ConfigurationDiffer::class)->diff($previousSnapshot, $application->refresh()->deploymentConfigurationSnapshot())->requiresBuild())->toBeFalse();
|
||||
});
|
||||
|
||||
it('does not report newly tracked settings when an older snapshot omitted their default values', function () {
|
||||
$application = snapshotTestApplication();
|
||||
$application->settings->update(['stop_grace_period' => DEFAULT_STOP_GRACE_PERIOD_SECONDS]);
|
||||
$currentSnapshot = $application->deploymentConfigurationSnapshot();
|
||||
$introducedKeys = [
|
||||
'is_static',
|
||||
'is_spa',
|
||||
'is_git_submodules_enabled',
|
||||
'is_git_lfs_enabled',
|
||||
'is_git_shallow_clone_enabled',
|
||||
'is_env_sorting_enabled',
|
||||
'is_consistent_container_name_enabled',
|
||||
'is_container_label_escape_enabled',
|
||||
'is_container_label_readonly_enabled',
|
||||
'is_log_drain_enabled',
|
||||
'is_swarm_only_worker_nodes',
|
||||
'is_preserve_repository_enabled',
|
||||
'stop_grace_period',
|
||||
];
|
||||
$previousSnapshot = $currentSnapshot;
|
||||
|
||||
foreach (['build', 'runtime'] as $section) {
|
||||
data_set(
|
||||
$previousSnapshot,
|
||||
"sections.{$section}.items",
|
||||
collect(data_get($previousSnapshot, "sections.{$section}.items"))
|
||||
->reject(fn (array $item): bool => in_array($item['key'], $introducedKeys, true))
|
||||
->values()
|
||||
->all(),
|
||||
);
|
||||
}
|
||||
|
||||
expect(app(ConfigurationDiffer::class)->diff($previousSnapshot, $currentSnapshot)->isChanged())->toBeFalse();
|
||||
});
|
||||
|
||||
it('accepts the historical environment sorting default in older snapshots', function () {
|
||||
$application = snapshotTestApplication();
|
||||
$application->settings->update(['is_env_sorting_enabled' => true]);
|
||||
$currentSnapshot = $application->deploymentConfigurationSnapshot();
|
||||
$previousSnapshot = $currentSnapshot;
|
||||
data_set(
|
||||
$previousSnapshot,
|
||||
'sections.build.items',
|
||||
collect(data_get($previousSnapshot, 'sections.build.items'))
|
||||
->reject(fn (array $item): bool => $item['key'] === 'is_env_sorting_enabled')
|
||||
->values()
|
||||
->all(),
|
||||
);
|
||||
|
||||
expect(app(ConfigurationDiffer::class)->diff($previousSnapshot, $currentSnapshot)->isChanged())->toBeFalse();
|
||||
});
|
||||
|
||||
it('detects environment variable value changes without exposing secret values', function () {
|
||||
$application = snapshotTestApplication();
|
||||
EnvironmentVariable::create([
|
||||
|
|
|
|||
Loading…
Reference in a new issue