Files
Art-gallery/infra/scripts/harmonize.ps1
T
Danila KhodjaefandCursor f247b418d8 Add dev-prod harmonize tool for bidirectional catalog and image sync.
Schema migrates dev to prod only; catalog rows merge by updated_at and images by mtime with union merge and conflict reporting.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-15 11:02:17 +03:00

235 lines
7.3 KiB
PowerShell

# 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