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>
This commit is contained in:
co-authored by
Cursor
parent
81ebad2120
commit
f247b418d8
@@ -0,0 +1,27 @@
|
||||
# Map SMB share for harmonize image step (optional wrapper).
|
||||
param(
|
||||
[switch]$SkipConfirm
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
. (Join-Path $PSScriptRoot "lib\Deploy-CliResult.ps1")
|
||||
|
||||
$smbRoot = "\\192.168.10.122\Gallery"
|
||||
if ($env:SMB_USER -and $env:SMB_PASSWORD) {
|
||||
Write-Host "Mapping $smbRoot ..."
|
||||
net use $smbRoot /user:$env:SMB_USER $env:SMB_PASSWORD 2>&1 | Out-Host
|
||||
} elseif (-not (Test-Path $smbRoot)) {
|
||||
Write-Warning "Cannot access $smbRoot (not authenticated)."
|
||||
Write-Warning "Map the share first or set `$env:SMB_USER and `$env:SMB_PASSWORD."
|
||||
}
|
||||
|
||||
$npmArgs = @('run', 'harmonize:images')
|
||||
if ($args.Count -gt 0) {
|
||||
$npmArgs += '--'
|
||||
$npmArgs += $args
|
||||
}
|
||||
|
||||
$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path
|
||||
Set-Location $RepoRoot
|
||||
& npm @npmArgs
|
||||
exit $LASTEXITCODE
|
||||
@@ -0,0 +1,234 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user