345 lines
12 KiB
Text
345 lines
12 KiB
Text
|
|
# Workspace Migration Framework
|
||
|
|
# Handles workspace migrations between versions with backups and rollback
|
||
|
|
|
||
|
|
use std log
|
||
|
|
use platform/user/config.nu [get-workspace-path validate-workspace-exists]
|
||
|
|
use domain/workspace/version.nu [
|
||
|
|
add-migration-record get-system-version get-workspace-metadata-path
|
||
|
|
init-workspace-metadata load-workspace-metadata
|
||
|
|
]
|
||
|
|
|
||
|
|
export def get-migration-strategies [] {
|
||
|
|
{
|
||
|
|
"unknown_to_2.0.5": {
|
||
|
|
name: "Initialize metadata"
|
||
|
|
description: "Add metadata tracking to existing workspace"
|
||
|
|
from: "unknown"
|
||
|
|
to: "2.0.5"
|
||
|
|
breaking: false
|
||
|
|
auto_migrate: true
|
||
|
|
}
|
||
|
|
"2.0.0_to_2.0.5": {
|
||
|
|
name: "Workspace switching update"
|
||
|
|
description: "Add workspace switching support and metadata"
|
||
|
|
from: "2.0.0"
|
||
|
|
to: "2.0.5"
|
||
|
|
breaking: false
|
||
|
|
auto_migrate: true
|
||
|
|
}
|
||
|
|
"2.0.5_to_3.0.0": {
|
||
|
|
name: "Major version update"
|
||
|
|
description: "Migrate to v3.0.0 with breaking changes"
|
||
|
|
from: "2.0.5"
|
||
|
|
to: "3.0.0"
|
||
|
|
breaking: true
|
||
|
|
auto_migrate: false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export def find-migration-path [
|
||
|
|
from_version: string
|
||
|
|
to_version: string
|
||
|
|
] {
|
||
|
|
let strategies = (get-migration-strategies)
|
||
|
|
mut path = []
|
||
|
|
|
||
|
|
if $from_version == "unknown" {
|
||
|
|
$path = ($path | append {
|
||
|
|
strategy: "unknown_to_2.0.5"
|
||
|
|
details: $strategies.unknown_to_2.0.5
|
||
|
|
})
|
||
|
|
return $path
|
||
|
|
}
|
||
|
|
|
||
|
|
let strategy_key = $"($from_version)_to_($to_version)"
|
||
|
|
if ($strategy_key in $strategies) {
|
||
|
|
$path = ($path | append {
|
||
|
|
strategy: $strategy_key
|
||
|
|
details: ($strategies | get $strategy_key)
|
||
|
|
})
|
||
|
|
return $path
|
||
|
|
}
|
||
|
|
|
||
|
|
return $path
|
||
|
|
}
|
||
|
|
|
||
|
|
export def create-workspace-backup [
|
||
|
|
workspace_path: string
|
||
|
|
backup_reason: string
|
||
|
|
] {
|
||
|
|
let workspace_name = ($workspace_path | path basename)
|
||
|
|
let timestamp = (date now | format date "%Y%m%d_%H%M%S")
|
||
|
|
let backup_name = $"($workspace_name)_backup_($timestamp)"
|
||
|
|
let backup_dir = ($workspace_path | path dirname | path join ".workspace_backups")
|
||
|
|
|
||
|
|
if not ($backup_dir | path exists) {
|
||
|
|
mkdir $backup_dir
|
||
|
|
}
|
||
|
|
|
||
|
|
let backup_path = ($backup_dir | path join $backup_name)
|
||
|
|
print $"(ansi cyan)Creating backup...(ansi reset)"
|
||
|
|
|
||
|
|
let backup_result = (do {
|
||
|
|
cp -r $workspace_path $backup_path
|
||
|
|
let backup_metadata = {
|
||
|
|
original_path: $workspace_path
|
||
|
|
backup_path: $backup_path
|
||
|
|
timestamp: $timestamp
|
||
|
|
reason: $backup_reason
|
||
|
|
created: (date now | format date "%Y-%m-%dT%H:%M:%SZ")
|
||
|
|
}
|
||
|
|
$backup_metadata | to yaml | save -f ($backup_path | path join ".backup_info.yaml")
|
||
|
|
{success: true, backup_path: $backup_path, metadata: $backup_metadata}
|
||
|
|
} | complete)
|
||
|
|
|
||
|
|
if $backup_result.exit_code == 0 {
|
||
|
|
print $"(ansi green)✓(ansi reset) Backup created: [$backup_path]"
|
||
|
|
$backup_result.stdout
|
||
|
|
} else {
|
||
|
|
print $"(ansi red)✗(ansi reset) Backup failed: ($backup_result.stderr)"
|
||
|
|
{ success: false, error: $backup_result.stderr }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export def migrate-unknown-to-2_0_5 [
|
||
|
|
workspace_path: string
|
||
|
|
workspace_name: string
|
||
|
|
] {
|
||
|
|
print $"(ansi cyan)Migrating workspace to version 2.0.5...(ansi reset)"
|
||
|
|
|
||
|
|
let result = (do {
|
||
|
|
let metadata = (init-workspace-metadata $workspace_path $workspace_name)
|
||
|
|
add-migration-record $workspace_path "unknown" "2.0.5" "metadata_initialization" true "Initial metadata creation"
|
||
|
|
{ success: true, message: "Workspace migrated to version 2.0.5", metadata: $metadata }
|
||
|
|
} | complete)
|
||
|
|
|
||
|
|
if $result.exit_code == 0 { $result.stdout } else {
|
||
|
|
{ success: false, error: $result.stderr, message: "Migration failed" }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export def migrate-2_0_0-to-2_0_5 [
|
||
|
|
workspace_path: string
|
||
|
|
workspace_name: string
|
||
|
|
] {
|
||
|
|
print $"(ansi cyan)Migrating workspace from 2.0.0 to 2.0.5...(ansi reset)"
|
||
|
|
|
||
|
|
let result = (do {
|
||
|
|
let metadata_path = (get-workspace-metadata-path $workspace_path)
|
||
|
|
if ($metadata_path | path exists) {
|
||
|
|
let metadata = (load-workspace-metadata $workspace_path)
|
||
|
|
add-migration-record $workspace_path "2.0.0" "2.0.5" "version_update" true "Updated to workspace switching support"
|
||
|
|
} else {
|
||
|
|
let metadata = (init-workspace-metadata $workspace_path $workspace_name)
|
||
|
|
add-migration-record $workspace_path "2.0.0" "2.0.5" "metadata_creation" true "Added metadata tracking"
|
||
|
|
}
|
||
|
|
{ success: true, message: "Workspace migrated to version 2.0.5" }
|
||
|
|
} | complete)
|
||
|
|
|
||
|
|
if $result.exit_code == 0 { $result.stdout } else {
|
||
|
|
{ success: false, error: $result.stderr, message: "Migration failed" }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export def execute-migration [
|
||
|
|
workspace_path: string
|
||
|
|
workspace_name: string
|
||
|
|
strategy: record
|
||
|
|
] {
|
||
|
|
print ""
|
||
|
|
print $"(ansi green_bold)Migration Strategy:(ansi reset) ($strategy.details.name)"
|
||
|
|
print $"(ansi cyan)Description:(ansi reset) ($strategy.details.description)"
|
||
|
|
print $"(ansi cyan)From:(ansi reset) ($strategy.details.from) → (ansi cyan)To:(ansi reset) ($strategy.details.to)"
|
||
|
|
print ""
|
||
|
|
|
||
|
|
match $strategy.strategy {
|
||
|
|
"unknown_to_2.0.5" => { migrate-unknown-to-2_0_5 $workspace_path $workspace_name }
|
||
|
|
"2.0.0_to_2.0.5" => { migrate-2_0_0-to-2_0_5 $workspace_path $workspace_name }
|
||
|
|
_ => { return { success: false, error: $"Unknown migration strategy: ($strategy.strategy)" } }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export def migrate-workspace [
|
||
|
|
workspace_name: string
|
||
|
|
--skip-backup (-s)
|
||
|
|
--force (-f)
|
||
|
|
--target-version: string
|
||
|
|
] {
|
||
|
|
print ""
|
||
|
|
print $"(ansi green_bold)Workspace Migration(ansi reset)"
|
||
|
|
print ""
|
||
|
|
|
||
|
|
if not (validate-workspace-exists $workspace_name) {
|
||
|
|
return { success: false, error: $"Workspace '($workspace_name)' not found in registry" }
|
||
|
|
}
|
||
|
|
|
||
|
|
let workspace_path = (get-workspace-path $workspace_name)
|
||
|
|
|
||
|
|
if not ($workspace_path | path exists) {
|
||
|
|
return { success: false, error: $"Workspace directory not found: ($workspace_path)" }
|
||
|
|
}
|
||
|
|
|
||
|
|
print $"(ansi cyan)Workspace:(ansi reset) ($workspace_name)"
|
||
|
|
print $"(ansi cyan)Path:(ansi reset) ($workspace_path)"
|
||
|
|
print ""
|
||
|
|
|
||
|
|
let metadata = (load-workspace-metadata $workspace_path)
|
||
|
|
let current_version = $metadata.version.provisioning
|
||
|
|
let system_version = (get-system-version)
|
||
|
|
let target = ($target_version | default $system_version)
|
||
|
|
|
||
|
|
print $"(ansi cyan)Current version:(ansi reset) ($current_version)"
|
||
|
|
print $"(ansi cyan)Target version:(ansi reset) ($target)"
|
||
|
|
print ""
|
||
|
|
|
||
|
|
let migration_path = (find-migration-path $current_version $target)
|
||
|
|
|
||
|
|
if ($migration_path | is-empty) {
|
||
|
|
if $current_version == $target {
|
||
|
|
return { success: true, message: "Workspace is already at target version", no_migration_needed: true }
|
||
|
|
} else {
|
||
|
|
return { success: false, error: $"No migration path found from ($current_version) to ($target)" }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
let has_breaking = ($migration_path | any { |m| $m.details.breaking })
|
||
|
|
if $has_breaking {
|
||
|
|
print $"(ansi yellow_bold)⚠ Warning: This migration includes breaking changes(ansi reset)"
|
||
|
|
print ""
|
||
|
|
}
|
||
|
|
|
||
|
|
if not $force {
|
||
|
|
print $"(ansi yellow)This will migrate the workspace from ($current_version) to ($target)(ansi reset)"
|
||
|
|
if not $skip_backup {
|
||
|
|
print $"(ansi cyan)A backup will be created before migration.(ansi reset)"
|
||
|
|
}
|
||
|
|
print ""
|
||
|
|
let response = (input "Continue with migration? (y/N): ")
|
||
|
|
if $response != "y" and $response != "Y" {
|
||
|
|
print $"(ansi yellow)Migration cancelled(ansi reset)"
|
||
|
|
return { success: false, cancelled: true }
|
||
|
|
}
|
||
|
|
print ""
|
||
|
|
}
|
||
|
|
|
||
|
|
mut backup_result = {success: false}
|
||
|
|
if not $skip_backup {
|
||
|
|
$backup_result = (create-workspace-backup $workspace_path "pre_migration")
|
||
|
|
if not $backup_result.success {
|
||
|
|
print $"(ansi red)✗(ansi reset) Cannot proceed without backup"
|
||
|
|
return { success: false, error: "Backup creation failed", backup_error: $backup_result.error }
|
||
|
|
}
|
||
|
|
print ""
|
||
|
|
}
|
||
|
|
|
||
|
|
mut migration_results = []
|
||
|
|
for migration in $migration_path {
|
||
|
|
let result = (execute-migration $workspace_path $workspace_name $migration)
|
||
|
|
$migration_results = ($migration_results | append $result)
|
||
|
|
|
||
|
|
if not $result.success {
|
||
|
|
print ""
|
||
|
|
print $"(ansi red_bold)✗ Migration failed(ansi reset)"
|
||
|
|
print $"(ansi red)Error:(ansi reset) ($result.error)"
|
||
|
|
print ""
|
||
|
|
if ($backup_result != null) {
|
||
|
|
print $"(ansi cyan)Backup available at:(ansi reset) ($backup_result.backup_path)"
|
||
|
|
}
|
||
|
|
print ""
|
||
|
|
return { success: false, error: $result.error, backup: $backup_result, partial_results: $migration_results }
|
||
|
|
}
|
||
|
|
print $"(ansi green)✓(ansi reset) ($migration.details.name) completed"
|
||
|
|
}
|
||
|
|
|
||
|
|
print ""
|
||
|
|
print $"(ansi green_bold)✓ Migration completed successfully(ansi reset)"
|
||
|
|
print ""
|
||
|
|
return { success: true, message: $"Workspace migrated from ($current_version) to ($target)", backup: $backup_result, migrations: $migration_results }
|
||
|
|
}
|
||
|
|
|
||
|
|
export def list-workspace-backups [
|
||
|
|
workspace_name?: string
|
||
|
|
] {
|
||
|
|
let workspace_path = if ($workspace_name | is-not-empty) {
|
||
|
|
get-workspace-path $workspace_name
|
||
|
|
} else {
|
||
|
|
null
|
||
|
|
}
|
||
|
|
|
||
|
|
let backup_base_dir = if ($workspace_path | is-not-empty) {
|
||
|
|
$workspace_path | path dirname | path join ".workspace_backups"
|
||
|
|
} else {
|
||
|
|
return []
|
||
|
|
}
|
||
|
|
|
||
|
|
if not ($backup_base_dir | path exists) { return [] }
|
||
|
|
|
||
|
|
ls $backup_base_dir
|
||
|
|
| where ($it.type == "dir")
|
||
|
|
| each { |backup|
|
||
|
|
let info_file = ($backup.name | path join ".backup_info.yaml")
|
||
|
|
if ($info_file | path exists) {
|
||
|
|
let info = (open $info_file)
|
||
|
|
{ name: ($backup.name | path basename), path: $backup.name, created: $info.created, reason: $info.reason, size: $backup.size }
|
||
|
|
} else {
|
||
|
|
{ name: ($backup.name | path basename), path: $backup.name, created: "unknown", reason: "unknown", size: $backup.size }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export def restore-workspace-from-backup [
|
||
|
|
backup_path: string
|
||
|
|
--force (-f)
|
||
|
|
] {
|
||
|
|
if not ($backup_path | path exists) {
|
||
|
|
return { success: false, error: $"Backup not found: ($backup_path)" }
|
||
|
|
}
|
||
|
|
|
||
|
|
let info_file = ($backup_path | path join ".backup_info.yaml")
|
||
|
|
if not ($info_file | path exists) {
|
||
|
|
return { success: false, error: "Backup metadata not found - cannot restore safely" }
|
||
|
|
}
|
||
|
|
|
||
|
|
let info = (open $info_file)
|
||
|
|
let original_path = $info.original_path
|
||
|
|
|
||
|
|
print ""
|
||
|
|
print $"(ansi green_bold)Restore Workspace from Backup(ansi reset)"
|
||
|
|
print ""
|
||
|
|
print $"(ansi cyan)Backup:(ansi reset) ($backup_path)"
|
||
|
|
print $"(ansi cyan)Original path:(ansi reset) ($original_path)"
|
||
|
|
print $"(ansi cyan)Created:(ansi reset) ($info.created)"
|
||
|
|
print $"(ansi cyan)Reason:(ansi reset) ($info.reason)"
|
||
|
|
print ""
|
||
|
|
|
||
|
|
if not $force {
|
||
|
|
print $"(ansi yellow)⚠ This will replace the current workspace at:(ansi reset)"
|
||
|
|
print $" ($original_path)"
|
||
|
|
print ""
|
||
|
|
let response = (input "Continue with restore? (y/N): ")
|
||
|
|
if $response != "y" and $response != "Y" {
|
||
|
|
print $"(ansi yellow)Restore cancelled(ansi reset)"
|
||
|
|
return { success: false, cancelled: true }
|
||
|
|
}
|
||
|
|
print ""
|
||
|
|
}
|
||
|
|
|
||
|
|
let result = (do {
|
||
|
|
if ($original_path | path exists) { rm -rf $original_path }
|
||
|
|
cp -r $backup_path $original_path
|
||
|
|
let restored_info = ($original_path | path join ".backup_info.yaml")
|
||
|
|
if ($restored_info | path exists) { rm $restored_info }
|
||
|
|
{ success: true, restored_path: $original_path }
|
||
|
|
} | complete)
|
||
|
|
|
||
|
|
if $result.exit_code == 0 {
|
||
|
|
print $"(ansi green)✓(ansi reset) Workspace restored from backup"
|
||
|
|
print ""
|
||
|
|
$result.stdout
|
||
|
|
} else {
|
||
|
|
print $"(ansi red)✗(ansi reset) Restore failed: ($result.stderr)"
|
||
|
|
{ success: false, error: $result.stderr }
|
||
|
|
}
|
||
|
|
}
|