# Dev <-> prod harmonize orchestrator (incremental merge; not full replace). # # Usage (from repo root): # npm run harmonize # npm run harmonize -- -DryRun # npm run harmonize -- -Config infra/deploy/harmonize.config.json # # Requires infra/deploy/harmonize.config.json (copy from harmonize.config.example.json) # or infra/deploy/devtoprod.config.json with optional "harmonize" section. param( [string]$Config = (Join-Path $PSScriptRoot "..\deploy\harmonize.config.json"), [switch]$DryRun ) $ErrorActionPreference = "Stop" . (Join-Path $PSScriptRoot "lib\Deploy-CliResult.ps1") $RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path Set-Location $RepoRoot $BannerWidth = 64 $CompletedSteps = [System.Collections.Generic.List[string]]::new() $FailedStep = $null function Write-HarmonizeBanner { param([bool]$Success, [string]$FailedAt = '') Write-Host '' Write-Host ('=' * $BannerWidth) if ($Success) { Write-Host ' HARMONIZE SUCCEEDED - dev and prod converged' Write-Host (' Steps: ' + ($CompletedSteps -join ', ')) } else { Write-Host " HARMONIZE FAILED at step: $FailedAt" if ($CompletedSteps.Count -gt 0) { Write-Host (' Completed: ' + ($CompletedSteps -join ', ')) } } Write-Host ('=' * $BannerWidth) } function Invoke-Npm { param([string]$Script, [string[]]$ExtraArgs = @()) $npmArgs = @('run', $Script) if ($ExtraArgs.Count -gt 0) { $npmArgs += '--' $npmArgs += $ExtraArgs } Write-Host ("Running npm: npm " + ($npmArgs -join ' ')) -ForegroundColor DarkGray $prevEap = $ErrorActionPreference $ErrorActionPreference = 'Continue' $output = & npm @npmArgs 2>&1 $exit = $LASTEXITCODE $ErrorActionPreference = $prevEap foreach ($line in $output) { Write-Host $line } return $exit } function Invoke-Step { param([string]$Name, [scriptblock]$Action) if ($DryRun) { Write-Host "[dry-run] Would run: $Name" $script:CompletedSteps.Add($Name) return $true } Write-Host '' Write-Host "--- Step: $Name ---" -ForegroundColor Cyan try { $code = & $Action if ($null -eq $code) { $code = if ($null -ne $LASTEXITCODE) { $LASTEXITCODE } else { 0 } } if ($code -ne 0) { $script:FailedStep = $Name return $false } $script:CompletedSteps.Add($Name) return $true } catch { Write-Host $_.Exception.Message -ForegroundColor Red $script:FailedStep = $Name return $false } } function Resolve-ConfigPath { if (Test-Path $Config) { return (Resolve-Path $Config).Path } $fallback = Join-Path $PSScriptRoot "..\deploy\devtoprod.config.json" if (Test-Path $fallback) { Write-Host "Using fallback config: $fallback" -ForegroundColor DarkYellow return (Resolve-Path $fallback).Path } throw "Missing harmonize config. Copy infra/deploy/harmonize.config.example.json to infra/deploy/harmonize.config.json" } function Get-HarmonizeSteps { param([object]$Raw) $harmonize = $Raw.harmonize if ($null -eq $harmonize) { $harmonize = $Raw } $schemaChanged = $false if ($null -ne $harmonize.schemaChanged) { $schemaChanged = [bool]$harmonize.schemaChanged } elseif ($null -ne $Raw.schemaChanged) { $schemaChanged = [bool]$Raw.schemaChanged } $steps = @{ backupDev = $true backupProd = $true schema = $schemaChanged db = $true images = $true verify = $false } $cfgSteps = $harmonize.steps if ($null -ne $cfgSteps) { foreach ($key in $cfgSteps.PSObject.Properties.Name) { $steps[$key] = [bool]$cfgSteps.$key } } return $steps } function Set-SmbCredentials { param([object]$Smb) if ($Smb.user -and $Smb.password) { $env:SMB_USER = [string]$Smb.user $env:SMB_PASSWORD = [string]$Smb.password } } function Test-ApiUrl { param([string]$Url) if (-not $Url) { return $true } try { $resp = Invoke-WebRequest -Uri $Url -UseBasicParsing -TimeoutSec 20 Write-Host " OK $Url ($($resp.StatusCode))" -ForegroundColor DarkGreen return $true } catch { Write-Host " FAIL $Url : $($_.Exception.Message)" -ForegroundColor Red return $false } } $configPath = Resolve-ConfigPath $raw = Get-Content -LiteralPath $configPath -Raw | ConvertFrom-Json $steps = Get-HarmonizeSteps -Raw $raw $harmonize = if ($raw.harmonize) { $raw.harmonize } else { $raw } if ($DryRun -or ($harmonize.dryRun -eq $true)) { $DryRun = $true $env:CONFIRM_PROD = '1' } Set-SmbCredentials -Smb $raw.smb if ($harmonize.smb) { Set-SmbCredentials -Smb $harmonize.smb } $preferArg = @() if ($harmonize.prefer) { $preferArg = @("--prefer=$($harmonize.prefer)") } $dryRunArgs = @() if ($DryRun) { $dryRunArgs = @('--dry-run') } Write-Host "Harmonize config: $configPath" Write-Host ("Steps enabled: " + (($steps.GetEnumerator() | Where-Object { $_.Value } | ForEach-Object { $_.Key }) -join ', ')) if (-not $DryRun) { $env:CONFIRM_PROD = '1' } if ($steps.backupDev) { if (-not (Invoke-Step 'backupDev' { Invoke-Npm 'dev:db:backup' })) { Write-HarmonizeBanner -Success $false -FailedAt 'backupDev' Write-DeployCliResult -Script 'harmonize' -Success $false -Summary 'Failed at backupDev.' exit 1 } } if ($steps.backupProd) { if (-not (Invoke-Step 'backupProd' { Invoke-Npm 'prod:db:backup' })) { Write-HarmonizeBanner -Success $false -FailedAt 'backupProd' Write-DeployCliResult -Script 'harmonize' -Success $false -Summary 'Failed at backupProd.' exit 1 } } if ($steps.schema) { if (-not (Invoke-Step 'schema' { Invoke-Npm 'harmonize:schema' })) { Write-HarmonizeBanner -Success $false -FailedAt 'schema' Write-DeployCliResult -Script 'harmonize' -Success $false -Summary 'Failed at schema migrate.' exit 1 } } if ($steps.db) { if (-not (Invoke-Step 'db' { Invoke-Npm 'harmonize:db' @dryRunArgs @preferArg })) { Write-HarmonizeBanner -Success $false -FailedAt 'db' Write-DeployCliResult -Script 'harmonize' -Success $false -Summary 'Failed at DB harmonize.' exit 1 } } if ($steps.images) { if (-not (Invoke-Step 'images' { Invoke-Npm 'harmonize:images' @dryRunArgs })) { Write-HarmonizeBanner -Success $false -FailedAt 'images' Write-DeployCliResult -Script 'harmonize' -Success $false -Summary 'Failed at image harmonize.' exit 1 } } if ($steps.verify -and -not $DryRun) { $verify = $harmonize.verify if (-not $verify) { $verify = $raw.verify } $ok = $true if (-not (Invoke-Step 'verify' { if ($verify.devUrl -and -not (Test-ApiUrl $verify.devUrl)) { return 1 } if ($verify.publicUrl -and -not (Test-ApiUrl $verify.publicUrl)) { return 1 } return 0 })) { Write-HarmonizeBanner -Success $false -FailedAt 'verify' Write-DeployCliResult -Script 'harmonize' -Success $false -Summary 'Verify step failed.' exit 1 } } Write-HarmonizeBanner -Success $true $summary = if ($DryRun) { 'Harmonize dry-run complete (no changes written).' } else { 'Harmonize complete.' } Write-DeployCliResult -Script 'harmonize' -Success $true -Summary $summary -Details @("Config: $configPath") exit 0