fix(previews): keep compose preview data out of production pending changes

Closes #12284
This commit is contained in:
Andras Bacsai
2026-10-23 19:46:44 +02:00
parent cb561d3d9c
commit dcbad815fe
9 changed files with 182 additions and 110 deletions
+2 -2
View File
@@ -1507,7 +1507,7 @@ class Application extends BaseModel
$previousSnapshot = [];
}
return app(ConfigurationDiffer::class)->diff($previousSnapshot, $currentSnapshot);
return app(ConfigurationDiffer::class)->diff(ApplicationConfigurationSnapshot::withoutPreviewVolumes($previousSnapshot, $this), $currentSnapshot);
}
public function hasPendingDeploymentConfigurationChanges(): bool
@@ -1551,7 +1551,7 @@ class Application extends BaseModel
'configuration_hash' => $hash,
'configuration_snapshot' => $snapshot,
'configuration_diff' => $previousDeployment?->configuration_snapshot
? app(ConfigurationDiffer::class)->diff($previousDeployment->configuration_snapshot, $snapshot)->toArray()
? app(ConfigurationDiffer::class)->diff(ApplicationConfigurationSnapshot::withoutPreviewVolumes($previousDeployment->configuration_snapshot, $this), $snapshot)->toArray()
: null,
]);
@@ -100,6 +100,36 @@ class ApplicationConfigurationSnapshot
];
}
/**
* Removes the volumes of Docker Compose preview deployments from a snapshot. Older snapshots
* can contain them, because the parser stores preview volumes on the application.
*
* @param array<string, mixed> $snapshot
* @return array<string, mixed>
*/
public static function withoutPreviewVolumes(array $snapshot, Application $application): array
{
if (! is_array(data_get($snapshot, 'sections.storage.items'))) {
return $snapshot;
}
$snapshot['sections']['storage']['items'] = collect($snapshot['sections']['storage']['items'])
->reject(fn (array $item): bool => str_starts_with((string) data_get($item, 'key'), 'volume_')
&& self::isPreviewVolumeName((string) data_get($item, 'compare_value.source'), $application))
->values()
->all();
return $snapshot;
}
/**
* The compose parser names preview volumes "{uuid}_{volume}-pr-{id}" ("{uuid}-{volume}-pr-{id}" before parser version 3).
*/
private static function isPreviewVolumeName(string $name, Application $application): bool
{
return preg_match('/^'.preg_quote($application->uuid, '/').'[_-].+-pr-\d+$/', $name) === 1;
}
/**
* @return array<int, array<string, mixed>>
*/
@@ -237,6 +267,7 @@ class ApplicationConfigurationSnapshot
$volumes = $this->application->persistentStorages()
->orderBy('id')
->get(['id', 'name', 'mount_path', 'host_path'])
->reject(fn (LocalPersistentVolume $volume): bool => blank($volume->host_path) && self::isPreviewVolumeName($volume->name, $this->application))
->map(function (LocalPersistentVolume $volume): array {
$source = $volume->host_path ?: $volume->name;
+9 -4
View File
@@ -2018,6 +2018,10 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
$serviceEnvKey = str(normalizeComposeServiceName((string) $forServiceName))->upper();
$coolifyEnvironments->put('SERVICE_URL_'.$serviceEnvKey, $coolifyUrl->__toString());
$coolifyEnvironments->put('SERVICE_FQDN_'.$serviceEnvKey, $coolifyFqdn);
// A preview uses its own domains; the stored variables belong to the main application.
if ($isPullRequest) {
continue;
}
$resource->environment_variables()->updateOrCreate([
'resourceable_type' => Application::class,
'resourceable_id' => $resource->id,
@@ -2034,7 +2038,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
'value' => $coolifyFqdn,
'is_preview' => false,
]);
} else {
} elseif (! $isPullRequest) {
$resource->environment_variables()->where('resourceable_type', Application::class)
->where('resourceable_id', $resource->id)
->where('key', 'LIKE', "SERVICE_FQDN_{$serviceNameFormatted}%")
@@ -2099,7 +2103,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
if ($environment->count() > 0) {
$environment = $environment->filter(function ($value, $key) {
return ! str($key)->startsWith('SERVICE_FQDN_');
})->map(function ($value, $key) use ($resource) {
})->map(function ($value, $key) use ($resource, $isPullRequest) {
// Preserve empty strings and null values with correct Docker Compose semantics:
// - Empty string: Variable is set to "" (e.g., HTTP_PROXY="" means "no proxy")
// - Null: Variable is unset/removed from container environment (may inherit from host)
@@ -2107,8 +2111,9 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
// User explicitly wants variable unset - respect that
// NEVER override from database - null means "inherit from environment"
// Keep as null (will be excluded from container environment)
} elseif ($value === '') {
// Empty string - allow database override for backward compatibility
} elseif ($value === '' && ! ($isPullRequest && str($key)->startsWith('SERVICE_URL_'))) {
// Empty string - allow database override for backward compatibility.
// The stored service URLs belong to the main application, so a preview does not use them.
$dbEnv = $resource->environment_variables()->where('key', $key)->first();
// Only use database override if it exists AND has a non-empty value
if ($dbEnv && str($dbEnv->value)->isNotEmpty()) {
@@ -249,3 +249,17 @@ it('parses a preview with a bind mount in development mode', function () {
expect(json_encode($compose, JSON_UNESCAPED_SLASHES))->toContain('/config/app.conf-pr-42');
});
it('keeps the production service URL variables when it parses a preview', function () {
Process::fake();
$this->application->update(['docker_compose_domains' => json_encode(['web' => ['domain' => 'https://web.example.com']])]);
$this->application->fresh()->parse();
$this->preview->update(['docker_compose_domains' => json_encode(['web' => ['domain' => 'https://pr-42.example.com']])]);
$compose = $this->application->fresh()->parse(pull_request_id: 42, preview_id: $this->preview->id);
$productionVariables = $this->application->environment_variables()->pluck('value', 'key');
expect($productionVariables->get('SERVICE_URL_WEB'))->toBe('https://web.example.com')
->and($productionVariables->get('SERVICE_FQDN_WEB'))->toBe('web.example.com')
->and(json_encode($compose, JSON_UNESCAPED_SLASHES))->toContain('https://pr-42.example.com');
});
@@ -1386,26 +1386,26 @@
required: true
SELF_REF: null
SERVICE_BASE64_64_WEB: '<generated:SERVICE_BASE64_64_WEB:64>'
SERVICE_FQDN_ADMIN: 7.admin.example.com
SERVICE_FQDN_ADMIN: referenceapplication0001.10.0.0.1.sslip.io
SERVICE_FQDN_ADMIN_8080: null
SERVICE_FQDN_API: 7.www.api.example.com
SERVICE_FQDN_API_V2: 7.api-v2.example.com
SERVICE_FQDN_BLOG: 7.blog.example.com
SERVICE_FQDN_MY_APP: 7.my-app.example.com
SERVICE_FQDN_WEB: 7.web.example.com
SERVICE_FQDN_WEB_3000: null
SERVICE_FQDN_API: referenceapplication0001.10.0.0.1.sslip.io/api
SERVICE_FQDN_API_V2: api-v2.example.com
SERVICE_FQDN_BLOG: blog.example.com
SERVICE_FQDN_MY_APP: my-app.example.com
SERVICE_FQDN_WEB: web.example.com
SERVICE_FQDN_WEB_3000: 'web-referenceapplication0001.10.0.0.1.sslip.io:3000'
SERVICE_HEX_32_WEB: '<generated:SERVICE_HEX_32_WEB:32>'
SERVICE_PASSWORD_DB: '<generated:SERVICE_PASSWORD_DB:32>'
SERVICE_PASSWORD_NOBRACES: '<generated:SERVICE_PASSWORD_NOBRACES:32>'
SERVICE_PASSWORD_WEB: '<generated:SERVICE_PASSWORD_WEB:32>'
SERVICE_URL_ADMIN: 'https://7.admin.example.com'
SERVICE_URL_API: 'https://7.www.api.example.com'
SERVICE_URL_API_V2: 'https://7.api-v2.example.com'
SERVICE_URL_BLOG: 'https://7.blog.example.com/blog:2368'
SERVICE_URL_MY_APP: 'https://7.my-app.example.com'
SERVICE_URL_ADMIN: 'https://admin.example.com'
SERVICE_URL_API: 'https://www.api.example.com'
SERVICE_URL_API_V2: 'https://api-v2.example.com'
SERVICE_URL_BLOG: 'https://blog.example.com/blog:2368'
SERVICE_URL_MY_APP: 'https://my-app.example.com'
SERVICE_URL_MY_APP_80: null
SERVICE_URL_WEB: 'https://7.web.example.com'
SERVICE_URL_WEB_3000: null
SERVICE_URL_WEB: 'https://web.example.com'
SERVICE_URL_WEB_3000: 'http://web-referenceapplication0001.10.0.0.1.sslip.io:3000'
SERVICE_USER_DB: '<generated:SERVICE_USER_DB:16>'
SERVICE_USER_WEB: '<generated:SERVICE_USER_WEB:16>'
WITH_DEFAULT: null
@@ -2088,23 +2088,23 @@
SERVICE_FQDN_ADMIN: referenceapplication0001.10.0.0.1.sslip.io
SERVICE_FQDN_ADMIN_8080: null
SERVICE_FQDN_API: referenceapplication0001.10.0.0.1.sslip.io/api
SERVICE_FQDN_API_V2: 7.api-v2.example.com
SERVICE_FQDN_BLOG: 7.blog.example.com
SERVICE_FQDN_MY_APP: 7.my-app.example.com
SERVICE_FQDN_WEB: 7.web.example.com
SERVICE_FQDN_WEB_3000: null
SERVICE_FQDN_API_V2: api-v2.example.com
SERVICE_FQDN_BLOG: blog.example.com
SERVICE_FQDN_MY_APP: my-app.example.com
SERVICE_FQDN_WEB: web.example.com
SERVICE_FQDN_WEB_3000: 'web-referenceapplication0001.10.0.0.1.sslip.io:3000'
SERVICE_HEX_32_WEB: '<generated:SERVICE_HEX_32_WEB:32>'
SERVICE_PASSWORD_DB: '<generated:SERVICE_PASSWORD_DB:32>'
SERVICE_PASSWORD_NOBRACES: '<generated:SERVICE_PASSWORD_NOBRACES:32>'
SERVICE_PASSWORD_WEB: '<generated:SERVICE_PASSWORD_WEB:32>'
SERVICE_URL_ADMIN: 'https://7.admin.example.com'
SERVICE_URL_API: 'https://7.www.api.example.com'
SERVICE_URL_API_V2: 'https://7.api-v2.example.com'
SERVICE_URL_BLOG: 'https://7.blog.example.com/blog:2368'
SERVICE_URL_MY_APP: 'https://7.my-app.example.com'
SERVICE_URL_ADMIN: 'https://admin.example.com'
SERVICE_URL_API: 'https://www.api.example.com'
SERVICE_URL_API_V2: 'https://api-v2.example.com'
SERVICE_URL_BLOG: 'https://blog.example.com/blog:2368'
SERVICE_URL_MY_APP: 'https://my-app.example.com'
SERVICE_URL_MY_APP_80: null
SERVICE_URL_WEB: 'https://7.web.example.com'
SERVICE_URL_WEB_3000: null
SERVICE_URL_WEB: 'https://web.example.com'
SERVICE_URL_WEB_3000: 'http://web-referenceapplication0001.10.0.0.1.sslip.io:3000'
SERVICE_USER_DB: '<generated:SERVICE_USER_DB:16>'
SERVICE_USER_WEB: '<generated:SERVICE_USER_WEB:16>'
WITH_DEFAULT: null
@@ -1452,26 +1452,26 @@
required: true
SELF_REF: null
SERVICE_BASE64_64_WEB: '<generated:SERVICE_BASE64_64_WEB:64>'
SERVICE_FQDN_ADMIN: 7.admin.example.com
SERVICE_FQDN_ADMIN: referenceapplication0001.10.0.0.1.sslip.io
SERVICE_FQDN_ADMIN_8080: null
SERVICE_FQDN_API: 7.www.api.example.com
SERVICE_FQDN_API_V2: 7.api-v2.example.com
SERVICE_FQDN_BLOG: 7.blog.example.com
SERVICE_FQDN_MY_APP: 7.my-app.example.com
SERVICE_FQDN_WEB: 7.web.example.com
SERVICE_FQDN_WEB_3000: null
SERVICE_FQDN_API: referenceapplication0001.10.0.0.1.sslip.io/api
SERVICE_FQDN_API_V2: api-v2.example.com
SERVICE_FQDN_BLOG: blog.example.com
SERVICE_FQDN_MY_APP: my-app.example.com
SERVICE_FQDN_WEB: web.example.com
SERVICE_FQDN_WEB_3000: 'web-referenceapplication0001.10.0.0.1.sslip.io:3000'
SERVICE_HEX_32_WEB: '<generated:SERVICE_HEX_32_WEB:32>'
SERVICE_PASSWORD_DB: '<generated:SERVICE_PASSWORD_DB:32>'
SERVICE_PASSWORD_NOBRACES: '<generated:SERVICE_PASSWORD_NOBRACES:32>'
SERVICE_PASSWORD_WEB: '<generated:SERVICE_PASSWORD_WEB:32>'
SERVICE_URL_ADMIN: 'https://7.admin.example.com'
SERVICE_URL_API: 'https://7.www.api.example.com'
SERVICE_URL_API_V2: 'https://7.api-v2.example.com'
SERVICE_URL_BLOG: 'https://7.blog.example.com/blog:2368'
SERVICE_URL_MY_APP: 'https://7.my-app.example.com'
SERVICE_URL_ADMIN: 'https://admin.example.com'
SERVICE_URL_API: 'https://www.api.example.com'
SERVICE_URL_API_V2: 'https://api-v2.example.com'
SERVICE_URL_BLOG: 'https://blog.example.com/blog:2368'
SERVICE_URL_MY_APP: 'https://my-app.example.com'
SERVICE_URL_MY_APP_80: null
SERVICE_URL_WEB: 'https://7.web.example.com'
SERVICE_URL_WEB_3000: null
SERVICE_URL_WEB: 'https://web.example.com'
SERVICE_URL_WEB_3000: 'http://web-referenceapplication0001.10.0.0.1.sslip.io:3000'
SERVICE_USER_DB: '<generated:SERVICE_USER_DB:16>'
SERVICE_USER_WEB: '<generated:SERVICE_USER_WEB:16>'
WITH_DEFAULT: null
@@ -2205,23 +2205,23 @@
SERVICE_FQDN_ADMIN: referenceapplication0001.10.0.0.1.sslip.io
SERVICE_FQDN_ADMIN_8080: null
SERVICE_FQDN_API: referenceapplication0001.10.0.0.1.sslip.io/api
SERVICE_FQDN_API_V2: 7.api-v2.example.com
SERVICE_FQDN_BLOG: 7.blog.example.com
SERVICE_FQDN_MY_APP: 7.my-app.example.com
SERVICE_FQDN_WEB: 7.web.example.com
SERVICE_FQDN_WEB_3000: null
SERVICE_FQDN_API_V2: api-v2.example.com
SERVICE_FQDN_BLOG: blog.example.com
SERVICE_FQDN_MY_APP: my-app.example.com
SERVICE_FQDN_WEB: web.example.com
SERVICE_FQDN_WEB_3000: 'web-referenceapplication0001.10.0.0.1.sslip.io:3000'
SERVICE_HEX_32_WEB: '<generated:SERVICE_HEX_32_WEB:32>'
SERVICE_PASSWORD_DB: '<generated:SERVICE_PASSWORD_DB:32>'
SERVICE_PASSWORD_NOBRACES: '<generated:SERVICE_PASSWORD_NOBRACES:32>'
SERVICE_PASSWORD_WEB: '<generated:SERVICE_PASSWORD_WEB:32>'
SERVICE_URL_ADMIN: 'https://7.admin.example.com'
SERVICE_URL_API: 'https://7.www.api.example.com'
SERVICE_URL_API_V2: 'https://7.api-v2.example.com'
SERVICE_URL_BLOG: 'https://7.blog.example.com/blog:2368'
SERVICE_URL_MY_APP: 'https://7.my-app.example.com'
SERVICE_URL_ADMIN: 'https://admin.example.com'
SERVICE_URL_API: 'https://www.api.example.com'
SERVICE_URL_API_V2: 'https://api-v2.example.com'
SERVICE_URL_BLOG: 'https://blog.example.com/blog:2368'
SERVICE_URL_MY_APP: 'https://my-app.example.com'
SERVICE_URL_MY_APP_80: null
SERVICE_URL_WEB: 'https://7.web.example.com'
SERVICE_URL_WEB_3000: null
SERVICE_URL_WEB: 'https://web.example.com'
SERVICE_URL_WEB_3000: 'http://web-referenceapplication0001.10.0.0.1.sslip.io:3000'
SERVICE_USER_DB: '<generated:SERVICE_USER_DB:16>'
SERVICE_USER_WEB: '<generated:SERVICE_USER_WEB:16>'
WITH_DEFAULT: null
@@ -1648,26 +1648,26 @@
required: true
SELF_REF: null
SERVICE_BASE64_64_WEB: '<generated:SERVICE_BASE64_64_WEB:64>'
SERVICE_FQDN_ADMIN: 7.admin.example.com
SERVICE_FQDN_ADMIN: referenceapplication0001.10.0.0.1.sslip.io
SERVICE_FQDN_ADMIN_8080: null
SERVICE_FQDN_API: 7.www.api.example.com
SERVICE_FQDN_API_V2: 7.api-v2.example.com
SERVICE_FQDN_BLOG: 7.blog.example.com
SERVICE_FQDN_MY_APP: 7.my-app.example.com
SERVICE_FQDN_WEB: 7.web.example.com
SERVICE_FQDN_WEB_3000: null
SERVICE_FQDN_API: referenceapplication0001.10.0.0.1.sslip.io/api
SERVICE_FQDN_API_V2: api-v2.example.com
SERVICE_FQDN_BLOG: blog.example.com
SERVICE_FQDN_MY_APP: my-app.example.com
SERVICE_FQDN_WEB: web.example.com
SERVICE_FQDN_WEB_3000: 'web-referenceapplication0001.10.0.0.1.sslip.io:3000'
SERVICE_HEX_32_WEB: '<generated:SERVICE_HEX_32_WEB:32>'
SERVICE_PASSWORD_DB: '<generated:SERVICE_PASSWORD_DB:32>'
SERVICE_PASSWORD_NOBRACES: '<generated:SERVICE_PASSWORD_NOBRACES:32>'
SERVICE_PASSWORD_WEB: '<generated:SERVICE_PASSWORD_WEB:32>'
SERVICE_URL_ADMIN: 'https://7.admin.example.com'
SERVICE_URL_API: 'https://7.www.api.example.com'
SERVICE_URL_API_V2: 'https://7.api-v2.example.com'
SERVICE_URL_BLOG: 'https://7.blog.example.com/blog:2368'
SERVICE_URL_MY_APP: 'https://7.my-app.example.com'
SERVICE_URL_ADMIN: 'https://admin.example.com'
SERVICE_URL_API: 'https://www.api.example.com'
SERVICE_URL_API_V2: 'https://api-v2.example.com'
SERVICE_URL_BLOG: 'https://blog.example.com/blog:2368'
SERVICE_URL_MY_APP: 'https://my-app.example.com'
SERVICE_URL_MY_APP_80: null
SERVICE_URL_WEB: 'https://7.web.example.com'
SERVICE_URL_WEB_3000: null
SERVICE_URL_WEB: 'https://web.example.com'
SERVICE_URL_WEB_3000: 'http://web-referenceapplication0001.10.0.0.1.sslip.io:3000'
SERVICE_USER_DB: '<generated:SERVICE_USER_DB:16>'
SERVICE_USER_WEB: '<generated:SERVICE_USER_WEB:16>'
WITH_DEFAULT: null
@@ -2511,23 +2511,23 @@
SERVICE_FQDN_ADMIN: referenceapplication0001.10.0.0.1.sslip.io
SERVICE_FQDN_ADMIN_8080: null
SERVICE_FQDN_API: referenceapplication0001.10.0.0.1.sslip.io/api
SERVICE_FQDN_API_V2: 7.api-v2.example.com
SERVICE_FQDN_BLOG: 7.blog.example.com
SERVICE_FQDN_MY_APP: 7.my-app.example.com
SERVICE_FQDN_WEB: 7.web.example.com
SERVICE_FQDN_WEB_3000: null
SERVICE_FQDN_API_V2: api-v2.example.com
SERVICE_FQDN_BLOG: blog.example.com
SERVICE_FQDN_MY_APP: my-app.example.com
SERVICE_FQDN_WEB: web.example.com
SERVICE_FQDN_WEB_3000: 'web-referenceapplication0001.10.0.0.1.sslip.io:3000'
SERVICE_HEX_32_WEB: '<generated:SERVICE_HEX_32_WEB:32>'
SERVICE_PASSWORD_DB: '<generated:SERVICE_PASSWORD_DB:32>'
SERVICE_PASSWORD_NOBRACES: '<generated:SERVICE_PASSWORD_NOBRACES:32>'
SERVICE_PASSWORD_WEB: '<generated:SERVICE_PASSWORD_WEB:32>'
SERVICE_URL_ADMIN: 'https://7.admin.example.com'
SERVICE_URL_API: 'https://7.www.api.example.com'
SERVICE_URL_API_V2: 'https://7.api-v2.example.com'
SERVICE_URL_BLOG: 'https://7.blog.example.com/blog:2368'
SERVICE_URL_MY_APP: 'https://7.my-app.example.com'
SERVICE_URL_ADMIN: 'https://admin.example.com'
SERVICE_URL_API: 'https://www.api.example.com'
SERVICE_URL_API_V2: 'https://api-v2.example.com'
SERVICE_URL_BLOG: 'https://blog.example.com/blog:2368'
SERVICE_URL_MY_APP: 'https://my-app.example.com'
SERVICE_URL_MY_APP_80: null
SERVICE_URL_WEB: 'https://7.web.example.com'
SERVICE_URL_WEB_3000: null
SERVICE_URL_WEB: 'https://web.example.com'
SERVICE_URL_WEB_3000: 'http://web-referenceapplication0001.10.0.0.1.sslip.io:3000'
SERVICE_USER_DB: '<generated:SERVICE_USER_DB:16>'
SERVICE_USER_WEB: '<generated:SERVICE_USER_WEB:16>'
WITH_DEFAULT: null
+26 -26
View File
@@ -1530,26 +1530,26 @@
required: true
SELF_REF: null
SERVICE_BASE64_64_WEB: '<generated:SERVICE_BASE64_64_WEB:64>'
SERVICE_FQDN_ADMIN: 7.admin.example.com
SERVICE_FQDN_ADMIN: referenceapplication0001.10.0.0.1.sslip.io
SERVICE_FQDN_ADMIN_8080: null
SERVICE_FQDN_API: 7.www.api.example.com
SERVICE_FQDN_API_V2: 7.api-v2.example.com
SERVICE_FQDN_BLOG: 7.blog.example.com
SERVICE_FQDN_MY_APP: 7.my-app.example.com
SERVICE_FQDN_WEB: 7.web.example.com
SERVICE_FQDN_WEB_3000: null
SERVICE_FQDN_API: referenceapplication0001.10.0.0.1.sslip.io/api
SERVICE_FQDN_API_V2: api-v2.example.com
SERVICE_FQDN_BLOG: blog.example.com
SERVICE_FQDN_MY_APP: my-app.example.com
SERVICE_FQDN_WEB: web.example.com
SERVICE_FQDN_WEB_3000: 'web-referenceapplication0001.10.0.0.1.sslip.io:3000'
SERVICE_HEX_32_WEB: '<generated:SERVICE_HEX_32_WEB:32>'
SERVICE_PASSWORD_DB: '<generated:SERVICE_PASSWORD_DB:32>'
SERVICE_PASSWORD_NOBRACES: '<generated:SERVICE_PASSWORD_NOBRACES:32>'
SERVICE_PASSWORD_WEB: '<generated:SERVICE_PASSWORD_WEB:32>'
SERVICE_URL_ADMIN: 'https://7.admin.example.com'
SERVICE_URL_API: 'https://7.www.api.example.com'
SERVICE_URL_API_V2: 'https://7.api-v2.example.com'
SERVICE_URL_BLOG: 'https://7.blog.example.com/blog:2368'
SERVICE_URL_MY_APP: 'https://7.my-app.example.com'
SERVICE_URL_ADMIN: 'https://admin.example.com'
SERVICE_URL_API: 'https://www.api.example.com'
SERVICE_URL_API_V2: 'https://api-v2.example.com'
SERVICE_URL_BLOG: 'https://blog.example.com/blog:2368'
SERVICE_URL_MY_APP: 'https://my-app.example.com'
SERVICE_URL_MY_APP_80: null
SERVICE_URL_WEB: 'https://7.web.example.com'
SERVICE_URL_WEB_3000: null
SERVICE_URL_WEB: 'https://web.example.com'
SERVICE_URL_WEB_3000: 'http://web-referenceapplication0001.10.0.0.1.sslip.io:3000'
SERVICE_USER_DB: '<generated:SERVICE_USER_DB:16>'
SERVICE_USER_WEB: '<generated:SERVICE_USER_WEB:16>'
WITH_DEFAULT: null
@@ -2331,23 +2331,23 @@
SERVICE_FQDN_ADMIN: referenceapplication0001.10.0.0.1.sslip.io
SERVICE_FQDN_ADMIN_8080: null
SERVICE_FQDN_API: referenceapplication0001.10.0.0.1.sslip.io/api
SERVICE_FQDN_API_V2: 7.api-v2.example.com
SERVICE_FQDN_BLOG: 7.blog.example.com
SERVICE_FQDN_MY_APP: 7.my-app.example.com
SERVICE_FQDN_WEB: 7.web.example.com
SERVICE_FQDN_WEB_3000: null
SERVICE_FQDN_API_V2: api-v2.example.com
SERVICE_FQDN_BLOG: blog.example.com
SERVICE_FQDN_MY_APP: my-app.example.com
SERVICE_FQDN_WEB: web.example.com
SERVICE_FQDN_WEB_3000: 'web-referenceapplication0001.10.0.0.1.sslip.io:3000'
SERVICE_HEX_32_WEB: '<generated:SERVICE_HEX_32_WEB:32>'
SERVICE_PASSWORD_DB: '<generated:SERVICE_PASSWORD_DB:32>'
SERVICE_PASSWORD_NOBRACES: '<generated:SERVICE_PASSWORD_NOBRACES:32>'
SERVICE_PASSWORD_WEB: '<generated:SERVICE_PASSWORD_WEB:32>'
SERVICE_URL_ADMIN: 'https://7.admin.example.com'
SERVICE_URL_API: 'https://7.www.api.example.com'
SERVICE_URL_API_V2: 'https://7.api-v2.example.com'
SERVICE_URL_BLOG: 'https://7.blog.example.com/blog:2368'
SERVICE_URL_MY_APP: 'https://7.my-app.example.com'
SERVICE_URL_ADMIN: 'https://admin.example.com'
SERVICE_URL_API: 'https://www.api.example.com'
SERVICE_URL_API_V2: 'https://api-v2.example.com'
SERVICE_URL_BLOG: 'https://blog.example.com/blog:2368'
SERVICE_URL_MY_APP: 'https://my-app.example.com'
SERVICE_URL_MY_APP_80: null
SERVICE_URL_WEB: 'https://7.web.example.com'
SERVICE_URL_WEB_3000: null
SERVICE_URL_WEB: 'https://web.example.com'
SERVICE_URL_WEB_3000: 'http://web-referenceapplication0001.10.0.0.1.sslip.io:3000'
SERVICE_USER_DB: '<generated:SERVICE_USER_DB:16>'
SERVICE_USER_WEB: '<generated:SERVICE_USER_WEB:16>'
WITH_DEFAULT: null
@@ -406,3 +406,25 @@ it('describes added unlocked environment variables with their value', function (
->and($change['new_display_value'])->toBe('new-secret')
->and(json_encode($diff->toArray()))->toContain('new-secret');
});
it('does not report Docker Compose preview volumes as pending production changes', function () {
$application = snapshotTestApplication(['build_pack' => 'dockercompose']);
$previewVolume = fn (): LocalPersistentVolume => LocalPersistentVolume::create([
'name' => $application->uuid.'_data-pr-42',
'mount_path' => '/app/data',
'resource_id' => $application->id,
'resource_type' => $application->getMorphClass(),
]);
// A preview deployment adds its volume after the production deployment.
markSnapshotTestApplicationDeployed($application);
$volume = $previewVolume();
expect($application->refresh()->pendingDeploymentConfigurationDiff()->isChanged())->toBeFalse();
// An older production snapshot contains the volume, and the preview cleanup removes it.
$volume->delete();
$previewVolume();
markSnapshotTestApplicationDeployed($application);
LocalPersistentVolume::query()->where('name', $application->uuid.'_data-pr-42')->delete();
expect($application->refresh()->pendingDeploymentConfigurationDiff()->isChanged())->toBeFalse();
});