mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-04 10:42:23 +00:00
Add encrypted env var backup in metadata.json for rebuild rollbacks
- Store encrypted build and runtime environment variables in metadata.json - Runtime vars duplicated in .env (plain text for docker compose) and metadata.json (encrypted backup for rebuild if .env deleted) - Build vars only in metadata.json (needed for exact image reproduction) - Add rollback_deployment_uuid column to track which deployment is being rolled back to during rebuild rollbacks 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b227619cbf
commit
45f931ecc8
9 files changed
+391
-65
No files matched your search
@@ -18,7 +18,8 @@ class SaveDeploymentSnapshot
|
||||
string $dockerComposeContent,
|
||||
string $envContent,
|
||||
?string $dockerfileContent = null,
|
||||
?string $productionImageName = null
|
||||
?string $productionImageName = null,
|
||||
?string $configHash = null
|
||||
): bool {
|
||||
$deploymentDir = deployment_configuration_dir($application->uuid, $deployment->deployment_uuid);
|
||||
$deploymentsBaseDir = deployments_base_dir($application->uuid);
|
||||
@@ -51,7 +52,7 @@ class SaveDeploymentSnapshot
|
||||
}
|
||||
|
||||
// Generate and save metadata
|
||||
$metadata = $this->generateMetadata($application, $deployment, $productionImageName);
|
||||
$metadata = $this->generateMetadata($application, $deployment, $productionImageName, $configHash);
|
||||
$metadataJson = json_encode($metadata, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
$metadataBase64 = base64_encode($metadataJson);
|
||||
instant_remote_process([
|
||||
@@ -73,7 +74,7 @@ class SaveDeploymentSnapshot
|
||||
return true;
|
||||
}
|
||||
|
||||
private function generateMetadata(Application $application, ApplicationDeploymentQueue $deployment, ?string $productionImageName = null): array
|
||||
private function generateMetadata(Application $application, ApplicationDeploymentQueue $deployment, ?string $productionImageName = null, ?string $configHash = null): array
|
||||
{
|
||||
// Use the actual production image name if provided (includes config hash for rollback support)
|
||||
// Fall back to commit-based name for backward compatibility with older deployments
|
||||
@@ -90,6 +91,7 @@ class SaveDeploymentSnapshot
|
||||
'branch' => $application->git_branch,
|
||||
'build_pack' => $application->build_pack,
|
||||
'image_name' => $imageName,
|
||||
'config_hash' => $configHash,
|
||||
'pull_request_id' => $deployment->pull_request_id,
|
||||
'git_repository' => $application->git_repository,
|
||||
'configuration_snapshot' => [
|
||||
@@ -106,7 +108,62 @@ class SaveDeploymentSnapshot
|
||||
'health_check_port' => $application->health_check_port,
|
||||
'limits_memory' => $application->limits_memory,
|
||||
'limits_cpus' => $application->limits_cpus,
|
||||
// Additional fields for rebuild rollback support
|
||||
'static_image' => $application->static_image,
|
||||
'dockerfile' => $application->dockerfile,
|
||||
'dockerfile_target_build' => $application->dockerfile_target_build,
|
||||
'custom_docker_run_options' => $application->custom_docker_run_options,
|
||||
'custom_labels' => $application->custom_labels,
|
||||
'docker_compose_custom_build_command' => $application->docker_compose_custom_build_command,
|
||||
'docker_compose_custom_start_command' => $application->docker_compose_custom_start_command,
|
||||
'docker_compose_location' => $application->docker_compose_location,
|
||||
],
|
||||
'settings_snapshot' => $this->serializeSettings($application),
|
||||
'build_environment_variables' => $this->serializeEnvironmentVariables($application, true),
|
||||
'runtime_environment_variables' => $this->serializeEnvironmentVariables($application, false),
|
||||
];
|
||||
}
|
||||
|
||||
private function serializeSettings(Application $application): array
|
||||
{
|
||||
$settings = $application->settings;
|
||||
if (! $settings) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
'use_build_secrets' => $settings->use_build_secrets ?? false,
|
||||
'inject_build_args_to_dockerfile' => $settings->inject_build_args_to_dockerfile ?? false,
|
||||
'include_source_commit_in_build' => $settings->include_source_commit_in_build ?? false,
|
||||
'disable_build_cache' => $settings->disable_build_cache ?? false,
|
||||
];
|
||||
}
|
||||
|
||||
private function serializeEnvironmentVariables(Application $application, bool $buildTime): array
|
||||
{
|
||||
// Query based on the appropriate field:
|
||||
// - For build-time vars: use is_buildtime = true
|
||||
// - For runtime vars: use is_runtime = true
|
||||
// This is important because a variable can have both flags set (used in both contexts)
|
||||
if ($buildTime) {
|
||||
$envVars = $application->environment_variables()
|
||||
->where('is_buildtime', true)
|
||||
->get();
|
||||
} else {
|
||||
$envVars = $application->environment_variables()
|
||||
->where('is_runtime', true)
|
||||
->get();
|
||||
}
|
||||
|
||||
return $envVars->map(function ($var) {
|
||||
return [
|
||||
'key' => $var->key,
|
||||
'value' => encrypt($var->real_value), // Encrypt value for secure storage on server
|
||||
'is_multiline' => $var->is_multiline ?? false,
|
||||
'is_literal' => $var->is_literal ?? false,
|
||||
'is_buildtime' => $var->is_buildtime ?? false,
|
||||
'is_runtime' => $var->is_runtime ?? true,
|
||||
];
|
||||
})->toArray();
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
private bool $rollback;
|
||||
|
||||
private ?string $rollback_deployment_uuid = null;
|
||||
|
||||
private ?array $rollback_snapshot = null;
|
||||
|
||||
private bool $force_rebuild;
|
||||
|
||||
private bool $restart_only;
|
||||
@@ -125,6 +129,13 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
*/
|
||||
private ?string $cachedConfigHash = null;
|
||||
|
||||
/**
|
||||
* Cached deployment timestamp computed once at the start of deployment.
|
||||
* Used for image tagging in pure Dockerfile deployments where no git commit exists.
|
||||
* Format: YmdHis (e.g., 20251219143025)
|
||||
*/
|
||||
private ?string $cachedDeploymentTimestamp = null;
|
||||
|
||||
private bool $is_debug_enabled;
|
||||
|
||||
private Collection|string $build_args;
|
||||
@@ -209,6 +220,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$this->pull_request_id = $this->application_deployment_queue->pull_request_id;
|
||||
$this->commit = $this->application_deployment_queue->commit;
|
||||
$this->rollback = $this->application_deployment_queue->rollback;
|
||||
$this->rollback_deployment_uuid = $this->application_deployment_queue->rollback_deployment_uuid;
|
||||
$this->disableBuildCache = $this->application->settings->disable_build_cache;
|
||||
$this->force_rebuild = $this->application_deployment_queue->force_rebuild;
|
||||
if ($this->disableBuildCache) {
|
||||
@@ -292,6 +304,15 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
try {
|
||||
// Make sure the private key is stored in the filesystem
|
||||
$this->server->privateKey->storeInFileSystem();
|
||||
|
||||
// Load and apply rollback snapshot if this is a rollback rebuild
|
||||
if ($this->rollback && $this->rollback_deployment_uuid) {
|
||||
$this->rollback_snapshot = $this->loadRollbackSnapshot();
|
||||
if ($this->rollback_snapshot) {
|
||||
$this->applyRollbackConfiguration();
|
||||
}
|
||||
}
|
||||
|
||||
// Generate custom host<->ip mapping
|
||||
$allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server);
|
||||
|
||||
@@ -499,6 +520,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
// This ensures the deployment status is FINISHED even if subsequent operations fail
|
||||
$this->completeDeployment();
|
||||
|
||||
// Save versioned deployment snapshot for rollback functionality
|
||||
// This must be called AFTER completeDeployment() sets status to FINISHED
|
||||
$this->save_deployment_snapshot();
|
||||
|
||||
// Then handle side effects - these should not fail the deployment
|
||||
try {
|
||||
GetContainersStatus::dispatch($this->server);
|
||||
@@ -1004,8 +1029,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
}
|
||||
|
||||
// Save versioned deployment snapshot for rollback functionality
|
||||
$this->save_deployment_snapshot();
|
||||
}
|
||||
|
||||
private function save_deployment_snapshot()
|
||||
@@ -1016,11 +1039,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if deployment failed or is not finished
|
||||
if ($this->application_deployment_queue->status !== ApplicationDeploymentStatus::FINISHED->value) {
|
||||
return;
|
||||
}
|
||||
|
||||
$serverForSnapshot = $this->use_build_server ? $this->original_server : $this->server;
|
||||
|
||||
// Get docker-compose content
|
||||
@@ -1058,7 +1076,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$dockerComposeContent,
|
||||
$envContent,
|
||||
$dockerfileContent,
|
||||
$this->production_image_name
|
||||
$this->production_image_name,
|
||||
$this->cachedConfigHash
|
||||
);
|
||||
|
||||
// Cleanup old deployments to maintain retention policy
|
||||
@@ -1161,12 +1180,18 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
private function generate_image_names()
|
||||
{
|
||||
if ($this->application->dockerfile) {
|
||||
// Pure Dockerfile apps have no git commit, use timestamp for unique tags
|
||||
// Include config hash for consistency (allows distinguishing same-time builds with different build args)
|
||||
$timestamp = $this->getDeploymentTimestamp();
|
||||
$configHash = $this->computeCurrentConfigHash();
|
||||
$imageTag = "{$timestamp}-{$configHash}";
|
||||
|
||||
if ($this->application->docker_registry_image_name) {
|
||||
$this->build_image_name = "{$this->application->docker_registry_image_name}:build";
|
||||
$this->production_image_name = "{$this->application->docker_registry_image_name}:latest";
|
||||
$this->build_image_name = "{$this->application->docker_registry_image_name}:{$imageTag}-build";
|
||||
$this->production_image_name = "{$this->application->docker_registry_image_name}:{$imageTag}";
|
||||
} else {
|
||||
$this->build_image_name = "{$this->application->uuid}:build";
|
||||
$this->production_image_name = "{$this->application->uuid}:latest";
|
||||
$this->build_image_name = "{$this->application->uuid}:{$imageTag}-build";
|
||||
$this->production_image_name = "{$this->application->uuid}:{$imageTag}";
|
||||
}
|
||||
} elseif ($this->application->build_pack === 'dockerimage') {
|
||||
// Check if this is an image hash deployment
|
||||
@@ -1230,6 +1255,180 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
return $this->cachedConfigHash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get deployment timestamp for image tagging.
|
||||
* Returns timestamp in YmdHis format (e.g., 20251219143025).
|
||||
*
|
||||
* This method caches the timestamp on first computation to ensure
|
||||
* the SAME timestamp is used throughout deployment for:
|
||||
* - Docker image tag
|
||||
* - docker-compose.yaml image reference
|
||||
* - metadata.json image_name field
|
||||
*
|
||||
* This guarantees rollback can find the correct image for each deployment.
|
||||
*/
|
||||
private function getDeploymentTimestamp(): string
|
||||
{
|
||||
if ($this->cachedDeploymentTimestamp !== null) {
|
||||
return $this->cachedDeploymentTimestamp;
|
||||
}
|
||||
|
||||
$this->cachedDeploymentTimestamp = now()->format('YmdHis');
|
||||
|
||||
return $this->cachedDeploymentTimestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the deployment snapshot from the server for rollback rebuild.
|
||||
* This reads the metadata.json file from the original deployment directory.
|
||||
*/
|
||||
private function loadRollbackSnapshot(): ?array
|
||||
{
|
||||
if (! $this->rollback || ! $this->rollback_deployment_uuid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$deploymentDir = deployment_configuration_dir(
|
||||
$this->application->uuid,
|
||||
$this->rollback_deployment_uuid
|
||||
);
|
||||
|
||||
$this->application_deployment_queue->addLogEntry("Loading configuration snapshot from deployment: {$this->rollback_deployment_uuid}");
|
||||
|
||||
$metadataJson = instant_remote_process([
|
||||
"cat {$deploymentDir}/metadata.json 2>/dev/null || echo '{}'",
|
||||
], $this->server, throwError: false);
|
||||
|
||||
$metadata = json_decode($metadataJson, true);
|
||||
|
||||
if (empty($metadata) || ! isset($metadata['version'])) {
|
||||
$this->application_deployment_queue->addLogEntry(
|
||||
'Warning: Rollback snapshot not found or invalid. Using current configuration.',
|
||||
'stderr'
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check version for compatibility
|
||||
if (version_compare($metadata['version'], '2.0', '<')) {
|
||||
$this->application_deployment_queue->addLogEntry(
|
||||
'Warning: Rollback snapshot is from an older version (v'.$metadata['version'].'). Some configuration may not be restored.',
|
||||
'stderr'
|
||||
);
|
||||
}
|
||||
|
||||
$this->application_deployment_queue->addLogEntry('Snapshot loaded successfully (version '.$metadata['version'].').');
|
||||
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the rollback snapshot configuration to the application.
|
||||
* This overrides application properties in memory (NOT saved to database).
|
||||
*/
|
||||
private function applyRollbackConfiguration(): void
|
||||
{
|
||||
if (! $this->rollback_snapshot) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->application_deployment_queue->addLogEntry('Applying configuration from rollback snapshot...');
|
||||
|
||||
$config = $this->rollback_snapshot['configuration_snapshot'] ?? [];
|
||||
|
||||
// Override application properties for this deployment
|
||||
// These changes are NOT saved to database - only used for this build
|
||||
$fieldsToApply = [
|
||||
'install_command', 'build_command', 'start_command',
|
||||
'dockerfile_location', 'base_directory', 'publish_directory',
|
||||
'static_image', 'dockerfile', 'dockerfile_target_build',
|
||||
'custom_docker_run_options', 'custom_labels',
|
||||
'docker_compose_custom_build_command', 'docker_compose_custom_start_command',
|
||||
'docker_compose_location', 'ports_exposes', 'ports_mappings',
|
||||
'health_check_path', 'health_check_port', 'limits_memory', 'limits_cpus',
|
||||
];
|
||||
|
||||
foreach ($fieldsToApply as $field) {
|
||||
if (array_key_exists($field, $config) && $config[$field] !== null) {
|
||||
$this->application->setAttribute($field, $config[$field]);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply settings snapshot
|
||||
$settingsSnapshot = $this->rollback_snapshot['settings_snapshot'] ?? [];
|
||||
if ($this->application->settings && ! empty($settingsSnapshot)) {
|
||||
foreach ($settingsSnapshot as $key => $value) {
|
||||
if ($value !== null) {
|
||||
$this->application->settings->setAttribute($key, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store config hash from snapshot for image tagging
|
||||
if (isset($this->rollback_snapshot['config_hash']) && $this->rollback_snapshot['config_hash']) {
|
||||
$this->cachedConfigHash = $this->rollback_snapshot['config_hash'];
|
||||
$this->application_deployment_queue->addLogEntry('Using config hash from snapshot: '.$this->cachedConfigHash);
|
||||
}
|
||||
|
||||
// Extract timestamp from snapshot image_name for pure Dockerfile rollbacks
|
||||
// Image format: {uuid}:{timestamp}-{configHash} (e.g., app:20251219143025-7f9e2c1a)
|
||||
if (isset($this->rollback_snapshot['image_name'])) {
|
||||
if (preg_match('/:(\d{14})-[a-f0-9]+$/', $this->rollback_snapshot['image_name'], $matches)) {
|
||||
$this->cachedDeploymentTimestamp = $matches[1];
|
||||
$this->application_deployment_queue->addLogEntry('Using timestamp from snapshot: '.$this->cachedDeploymentTimestamp);
|
||||
}
|
||||
}
|
||||
|
||||
$this->application_deployment_queue->addLogEntry('Rollback configuration applied.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get build environment variables from the rollback snapshot.
|
||||
* Returns null if no snapshot or no build vars in snapshot.
|
||||
*/
|
||||
private function getRollbackBuildEnvironmentVariables(): ?Collection
|
||||
{
|
||||
if (! $this->rollback_snapshot || ! isset($this->rollback_snapshot['build_environment_variables'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$vars = $this->rollback_snapshot['build_environment_variables'];
|
||||
if (empty($vars)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->application_deployment_queue->addLogEntry('Using '.count($vars).' build environment variables from rollback snapshot.');
|
||||
|
||||
return collect($vars)->map(function ($var) {
|
||||
$var['value'] = decrypt($var['value']); // Decrypt value from encrypted metadata
|
||||
|
||||
return (object) $var;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get runtime environment variables from the rollback snapshot.
|
||||
* Returns null if no snapshot or no runtime vars in snapshot.
|
||||
*/
|
||||
private function getRollbackRuntimeEnvironmentVariables(): ?Collection
|
||||
{
|
||||
if (! $this->rollback_snapshot || ! isset($this->rollback_snapshot['runtime_environment_variables'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$vars = $this->rollback_snapshot['runtime_environment_variables'];
|
||||
if (empty($vars)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return collect($vars)->map(function ($var) {
|
||||
$var['value'] = decrypt($var['value']); // Decrypt value from encrypted metadata
|
||||
|
||||
return (object) $var;
|
||||
});
|
||||
}
|
||||
|
||||
private function just_restart()
|
||||
{
|
||||
$this->application_deployment_queue->addLogEntry("Restarting {$this->customRepository}:{$this->application->git_branch} on {$this->server->name}.");
|
||||
@@ -1308,7 +1507,15 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
$envs = collect([]);
|
||||
$sort = $this->application->settings->is_env_sorting_enabled;
|
||||
if ($sort) {
|
||||
|
||||
// For rollback rebuilds, use runtime environment variables from the snapshot if available
|
||||
$rollbackRuntimeVars = $this->getRollbackRuntimeEnvironmentVariables();
|
||||
|
||||
if ($rollbackRuntimeVars !== null) {
|
||||
$this->application_deployment_queue->addLogEntry('Using '.count($rollbackRuntimeVars).' runtime environment variables from rollback snapshot.');
|
||||
$sorted_environment_variables = $sort ? $rollbackRuntimeVars->sortBy('key') : $rollbackRuntimeVars;
|
||||
$sorted_environment_variables_preview = collect([]); // No preview for rollback
|
||||
} elseif ($sort) {
|
||||
$sorted_environment_variables = $this->application->environment_variables->sortBy('key');
|
||||
$sorted_environment_variables_preview = $this->application->environment_variables_preview->sortBy('key');
|
||||
} else {
|
||||
@@ -1374,7 +1581,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
});
|
||||
|
||||
foreach ($runtime_environment_variables as $env) {
|
||||
$envs->push($env->key.'='.$env->real_value);
|
||||
// For snapshot variables, use 'value' directly (already decrypted)
|
||||
// For DB variables, use 'real_value' which handles decryption
|
||||
$envValue = property_exists($env, 'real_value') ? $env->real_value : ($env->value ?? '');
|
||||
$envs->push($env->key.'='.$envValue);
|
||||
}
|
||||
|
||||
// Check for PORT environment variable mismatch with ports_exposes
|
||||
@@ -1440,7 +1650,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
});
|
||||
|
||||
foreach ($runtime_environment_variables_preview as $env) {
|
||||
$envs->push($env->key.'='.$env->real_value);
|
||||
// For snapshot variables, use 'value' directly (already decrypted)
|
||||
// For DB variables, use 'real_value' which handles decryption
|
||||
$envValue = property_exists($env, 'real_value') ? $env->real_value : ($env->value ?? '');
|
||||
$envs->push($env->key.'='.$envValue);
|
||||
}
|
||||
// Add PORT if not exists, use the first port as default
|
||||
if ($this->build_pack !== 'dockercompose') {
|
||||
@@ -1676,8 +1889,12 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
|
||||
// 4. Add user-defined build-time variables LAST (highest priority - can override everything)
|
||||
// For rollback rebuilds, use variables from the snapshot if available
|
||||
$rollbackBuildVars = $this->getRollbackBuildEnvironmentVariables();
|
||||
|
||||
if ($this->pull_request_id === 0) {
|
||||
$sorted_environment_variables = $this->application->environment_variables()
|
||||
// Use snapshot variables if this is a rollback rebuild and snapshot has build vars
|
||||
$sorted_environment_variables = $rollbackBuildVars ?? $this->application->environment_variables()
|
||||
->where('is_buildtime', true) // ONLY build-time variables
|
||||
->orderBy($this->application->settings->is_env_sorting_enabled ? 'key' : 'id')
|
||||
->get();
|
||||
@@ -1690,10 +1907,14 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
|
||||
foreach ($sorted_environment_variables as $env) {
|
||||
// For snapshot variables, use 'value' directly (already decrypted)
|
||||
// For DB variables, use 'real_value' which handles decryption
|
||||
$rawValue = property_exists($env, 'real_value') ? $env->real_value : ($env->value ?? '');
|
||||
|
||||
// For literal/multiline vars, real_value includes quotes that we need to remove
|
||||
if ($env->is_literal || $env->is_multiline) {
|
||||
// Strip outer quotes from real_value and apply proper bash escaping
|
||||
$value = trim($env->real_value, "'");
|
||||
$value = trim($rawValue, "'");
|
||||
$escapedValue = escapeBashEnvValue($value);
|
||||
|
||||
if (isDev() && isset($envs_dict[$env->key])) {
|
||||
@@ -1705,13 +1926,12 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
if (isDev()) {
|
||||
$this->application_deployment_queue->addLogEntry("[DEBUG] Build-time env: {$env->key}");
|
||||
$this->application_deployment_queue->addLogEntry('[DEBUG] Type: literal/multiline');
|
||||
$this->application_deployment_queue->addLogEntry("[DEBUG] raw real_value: {$env->real_value}");
|
||||
$this->application_deployment_queue->addLogEntry("[DEBUG] stripped value: {$value}");
|
||||
$this->application_deployment_queue->addLogEntry("[DEBUG] final escaped: {$escapedValue}");
|
||||
}
|
||||
} else {
|
||||
// For normal vars, use double quotes to allow $VAR expansion
|
||||
$escapedValue = escapeBashDoubleQuoted($env->real_value);
|
||||
$escapedValue = escapeBashDoubleQuoted($rawValue);
|
||||
|
||||
if (isDev() && isset($envs_dict[$env->key])) {
|
||||
$this->application_deployment_queue->addLogEntry("[DEBUG] User override: {$env->key} (was: {$envs_dict[$env->key]}, now: {$escapedValue})");
|
||||
@@ -1722,7 +1942,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
if (isDev()) {
|
||||
$this->application_deployment_queue->addLogEntry("[DEBUG] Build-time env: {$env->key}");
|
||||
$this->application_deployment_queue->addLogEntry('[DEBUG] Type: normal (allows expansion)');
|
||||
$this->application_deployment_queue->addLogEntry("[DEBUG] real_value: {$env->real_value}");
|
||||
$this->application_deployment_queue->addLogEntry("[DEBUG] final escaped: {$escapedValue}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,6 +113,7 @@ class Rollback extends Component
|
||||
return [
|
||||
'deployment_uuid' => $deployment->deployment_uuid,
|
||||
'commit' => $deployment->commit,
|
||||
'commit_message' => $deployment->commitMessage(),
|
||||
'created_at' => $deployment->created_at,
|
||||
'has_config' => $hasConfig,
|
||||
'image_name' => $imageName,
|
||||
@@ -261,6 +262,7 @@ class Rollback extends Component
|
||||
commit: $commit,
|
||||
rollback: true,
|
||||
force_rebuild: true,
|
||||
rollback_deployment_uuid: $deploymentUuid,
|
||||
);
|
||||
|
||||
if ($result['status'] === 'queue_full') {
|
||||
|
||||
@@ -34,6 +34,7 @@ use OpenApi\Attributes as OA;
|
||||
'destination_id' => ['type' => 'string'],
|
||||
'only_this_server' => ['type' => 'boolean'],
|
||||
'rollback' => ['type' => 'boolean'],
|
||||
'rollback_deployment_uuid' => ['type' => 'string', 'nullable' => true],
|
||||
'commit_message' => ['type' => 'string'],
|
||||
],
|
||||
)]
|
||||
|
||||
@@ -11,7 +11,7 @@ use App\Models\StandaloneDocker;
|
||||
use Spatie\Url\Url;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
function queue_application_deployment(Application $application, string $deployment_uuid, ?int $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $is_api = false, bool $restart_only = false, ?string $git_type = null, bool $no_questions_asked = false, ?Server $server = null, ?StandaloneDocker $destination = null, bool $only_this_server = false, bool $rollback = false)
|
||||
function queue_application_deployment(Application $application, string $deployment_uuid, ?int $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $is_api = false, bool $restart_only = false, ?string $git_type = null, bool $no_questions_asked = false, ?Server $server = null, ?StandaloneDocker $destination = null, bool $only_this_server = false, bool $rollback = false, ?string $rollback_deployment_uuid = null)
|
||||
{
|
||||
$application_id = $application->id;
|
||||
$deployment_link = Url::fromString($application->link()."/deployment/{$deployment_uuid}");
|
||||
@@ -77,6 +77,7 @@ function queue_application_deployment(Application $application, string $deployme
|
||||
'restart_only' => $restart_only,
|
||||
'commit' => $commit,
|
||||
'rollback' => $rollback,
|
||||
'rollback_deployment_uuid' => $rollback_deployment_uuid,
|
||||
'git_type' => $git_type,
|
||||
'only_this_server' => $only_this_server,
|
||||
]);
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('application_deployment_queues', function (Blueprint $table) {
|
||||
$table->string('rollback_deployment_uuid')->nullable()->after('rollback');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('application_deployment_queues', function (Blueprint $table) {
|
||||
$table->dropColumn('rollback_deployment_uuid');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -10465,6 +10465,10 @@
|
||||
"rollback": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"rollback_deployment_uuid": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"commit_message": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
@@ -6637,6 +6637,9 @@ components:
|
||||
type: boolean
|
||||
rollback:
|
||||
type: boolean
|
||||
rollback_deployment_uuid:
|
||||
type: string
|
||||
nullable: true
|
||||
commit_message:
|
||||
type: string
|
||||
type: object
|
||||
|
||||
@@ -28,58 +28,75 @@
|
||||
<div wire:target='loadDeployments' wire:loading.remove>
|
||||
<div class="flex flex-col gap-2">
|
||||
@forelse ($deployments as $deployment)
|
||||
<div class="bg-white border rounded-sm dark:border-coolgray-300 dark:bg-coolgray-100 border-neutral-200 p-4">
|
||||
<div class="flex justify-between items-start">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
@if (data_get($deployment, 'is_current'))
|
||||
<span class="inline-flex items-center px-2 py-1 text-xs font-medium rounded-md bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
|
||||
CURRENT
|
||||
</span>
|
||||
@endif
|
||||
<span class="font-mono text-sm">
|
||||
{{ Str::limit(data_get($deployment, 'commit'), 12) }}
|
||||
</span>
|
||||
<span class="text-xs text-gray-400 dark:text-gray-500 font-mono">
|
||||
({{ data_get($deployment, 'deployment_uuid') }})
|
||||
</span>
|
||||
</div>
|
||||
@php
|
||||
$date = data_get($deployment, 'created_at');
|
||||
$interval = \Illuminate\Support\Carbon::parse($date);
|
||||
@endphp
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
{{ $interval->diffForHumans() }} ({{ $interval->format('Y-m-d H:i:s') }})
|
||||
</div>
|
||||
@if (data_get($deployment, 'image_name'))
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 font-mono mt-1">
|
||||
Image: {{ data_get($deployment, 'image_name') }}
|
||||
</div>
|
||||
@endif
|
||||
<div @class([
|
||||
'bg-white border rounded-sm dark:bg-coolgray-100 p-4 border-l-2',
|
||||
'border-l-green-500' => data_get($deployment, 'is_current'),
|
||||
'border-neutral-200 dark:border-coolgray-300' => !data_get($deployment, 'is_current'),
|
||||
])>
|
||||
<div class="flex flex-col gap-2">
|
||||
{{-- Row 1: Deployment path --}}
|
||||
<div class="text-xs text-neutral-500 dark:text-neutral-400 font-mono">
|
||||
/data/coolify/applications/{{ $application->uuid }}/deployments/{{ data_get($deployment, 'deployment_uuid') }}
|
||||
</div>
|
||||
<div class="flex flex-col items-end gap-2">
|
||||
@if (data_get($deployment, 'image_exists'))
|
||||
<span class="text-xs text-green-600 dark:text-green-400">Image available</span>
|
||||
|
||||
{{-- Row 3: Date --}}
|
||||
@php
|
||||
$date = data_get($deployment, 'created_at');
|
||||
$interval = \Illuminate\Support\Carbon::parse($date);
|
||||
@endphp
|
||||
<div class="text-sm text-neutral-500 dark:text-neutral-400">
|
||||
{{ $interval->format('Y-m-d H:i:s') }}
|
||||
</div>
|
||||
|
||||
{{-- Row 4: Image name (if available) --}}
|
||||
@if (data_get($deployment, 'image_name'))
|
||||
<div class="text-xs text-neutral-500 dark:text-neutral-400 font-mono">
|
||||
Image: {{ data_get($deployment, 'image_name') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Row 5: Commit info (git-based) OR Deployment time (pure Dockerfile) + Rollback button --}}
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
@if ($application->dockerfile && data_get($deployment, 'commit') === 'HEAD')
|
||||
{{-- Pure Dockerfile deployment - show deployment timestamp --}}
|
||||
<span class="text-sm text-neutral-500 dark:text-neutral-400">Deployed:</span>
|
||||
<span class="text-sm">{{ data_get($deployment, 'created_at')->format('Y-m-d H:i:s') }}</span>
|
||||
@else
|
||||
<span class="text-xs text-yellow-600 dark:text-yellow-400">Rebuild required</span>
|
||||
{{-- Git-based deployment - show commit info --}}
|
||||
<span class="text-sm text-neutral-500 dark:text-neutral-400">Commit:</span>
|
||||
<a href="{{ $application->gitCommitLink(data_get($deployment, 'commit')) }}"
|
||||
target="_blank"
|
||||
class="underline text-sm">
|
||||
{{ substr(data_get($deployment, 'commit'), 0, 7) }}
|
||||
</a>
|
||||
@if (data_get($deployment, 'commit_message'))
|
||||
<span class="text-neutral-500 dark:text-neutral-400">-</span>
|
||||
<a href="{{ $application->gitCommitLink(data_get($deployment, 'commit')) }}"
|
||||
target="_blank"
|
||||
class="text-neutral-500 dark:text-neutral-400 underline text-sm truncate max-w-md">
|
||||
{{ Str::before(data_get($deployment, 'commit_message'), "\n") }}
|
||||
</a>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
@if (!data_get($deployment, 'image_exists'))
|
||||
<span class="text-xs text-yellow-600 dark:text-yellow-400">(Rebuild required)</span>
|
||||
@endif
|
||||
|
||||
@can('deploy', $application)
|
||||
@if (data_get($deployment, 'is_current'))
|
||||
<x-forms.button disabled tooltip="This deployment is currently running.">
|
||||
Current
|
||||
</x-forms.button>
|
||||
@elseif (data_get($deployment, 'can_instant_rollback'))
|
||||
@if (data_get($deployment, 'can_instant_rollback'))
|
||||
<x-forms.button
|
||||
wire:click="rollbackToDeployment('{{ data_get($deployment, 'deployment_uuid') }}')"
|
||||
class="ml-auto"
|
||||
isHighlighted>
|
||||
Instant Rollback
|
||||
Rollback
|
||||
</x-forms.button>
|
||||
@elseif (data_get($deployment, 'can_rebuild_rollback'))
|
||||
<x-forms.button
|
||||
wire:click="rollbackToDeployment('{{ data_get($deployment, 'deployment_uuid') }}')"
|
||||
class="bg-yellow-600 hover:bg-yellow-700 dark:bg-yellow-700 dark:hover:bg-yellow-600">
|
||||
Rollback (Rebuild)
|
||||
class="ml-auto"
|
||||
isHighlighted>
|
||||
Rollback
|
||||
</x-forms.button>
|
||||
@endif
|
||||
@endcan
|
||||
@@ -87,7 +104,7 @@
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="text-gray-500 dark:text-gray-400">
|
||||
<div class="text-neutral-500 dark:text-neutral-400">
|
||||
No deployment history found. Deploy your application to enable rollback functionality.
|
||||
</div>
|
||||
@endforelse
|
||||
|
||||
Reference in new issue
Block a user