param( [string]$ZipPath, [string]$Target = "ubuntu@82.156.24.101", [string]$RemoteRoot = "/opt/kdl-olp-demo/8095", [string]$BaseUrl = "https://82.156.24.101:8095", [switch]$Sudo, [switch]$NoSudo, [switch]$SkipSmoke ) Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" $RepoRoot = Resolve-Path (Join-Path $PSScriptRoot "..\..\..") $DeployRoot = Join-Path $RepoRoot "kdl-wasm\web\test-results\deploy" if ([string]::IsNullOrWhiteSpace($ZipPath)) { $ZipPath = Get-ChildItem -LiteralPath $DeployRoot -Filter "kdl-olp-demo-*.zip" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty FullName } if ([string]::IsNullOrWhiteSpace($ZipPath) -or -not (Test-Path -LiteralPath $ZipPath)) { throw "Demo zip not found. Run: npm run demo:build" } $ZipPath = (Resolve-Path -LiteralPath $ZipPath).Path $ZipName = Split-Path -Leaf $ZipPath if ($ZipName -notmatch '^kdl-olp-demo-(.+)\.zip$') { throw "Zip name must match kdl-olp-demo-.zip: $ZipName" } $ReleaseId = $Matches[1] $RemoteZip = "/tmp/$ZipName" $RemoteRelease = "$RemoteRoot/releases/$ReleaseId" function Invoke-Remote { param([string]$Command) ssh -o StrictHostKeyChecking=accept-new $Target $Command if ($LASTEXITCODE -ne 0) { throw "Remote command failed: $Command" } } function Remote-Cmd { param([string]$Command) if ($script:UseSudo) { return "sudo sh -lc " + (Quote-Sh $Command) } return "sh -lc " + (Quote-Sh $Command) } function Quote-Sh { param([string]$Value) return "'" + ($Value -replace "'", "'\''") + "'" } function Get-RemoteParent { param([string]$Path) $Normalized = $Path.TrimEnd("/") $Index = $Normalized.LastIndexOf("/") if ($Index -le 0) { return "/" } return $Normalized.Substring(0, $Index) } $RemoteParent = Get-RemoteParent $RemoteRoot Write-Host "Deploying $ZipName to ${Target}:$RemoteRoot" scp -o StrictHostKeyChecking=accept-new $ZipPath "${Target}:$RemoteZip" if ($LASTEXITCODE -ne 0) { throw "scp upload failed" } $script:UseSudo = $false if ($Sudo) { $script:UseSudo = $true } elseif (-not $NoSudo) { ssh -o StrictHostKeyChecking=accept-new $Target "test -w $(Quote-Sh $RemoteParent)" if ($LASTEXITCODE -ne 0) { ssh -o StrictHostKeyChecking=accept-new $Target "sudo -n true" if ($LASTEXITCODE -eq 0) { $script:UseSudo = $true } else { throw "Remote root is not writable and passwordless sudo is unavailable. Re-run with a writable -RemoteRoot or configure sudo." } } } $prepare = @( "set -eu" "mkdir -p $(Quote-Sh "$RemoteRoot/releases")" "rm -rf $(Quote-Sh $RemoteRelease)" "mkdir -p $(Quote-Sh $RemoteRelease)" "unzip -q -o $(Quote-Sh $RemoteZip) -d $(Quote-Sh $RemoteRelease)" "test -f $(Quote-Sh "$RemoteRelease/index.html")" "test -f $(Quote-Sh "$RemoteRelease/demo-manifest.json")" "test -f $(Quote-Sh "$RemoteRelease/app/virtual-controller.html")" "if [ -L $(Quote-Sh "$RemoteRoot/current") ] || [ -e $(Quote-Sh "$RemoteRoot/current") ]; then rm -f $(Quote-Sh "$RemoteRoot/previous"); ln -sfn `"`$(readlink -f $(Quote-Sh "$RemoteRoot/current"))`" $(Quote-Sh "$RemoteRoot/previous"); fi" "ln -sfn $(Quote-Sh "$RemoteRelease") $(Quote-Sh "$RemoteRoot/current")" "rm -f $(Quote-Sh $RemoteZip)" ) -join "; " Invoke-Remote (Remote-Cmd $prepare) $restart = "if command -v systemctl >/dev/null 2>&1 && systemctl list-unit-files kdl-site-8095.service >/dev/null 2>&1; then systemctl restart kdl-site-8095; fi" Invoke-Remote (Remote-Cmd $restart) $smoke = @{ release_id = $ReleaseId zip = $ZipPath target = $Target remote_root = $RemoteRoot remote_release = $RemoteRelease base_url = $BaseUrl deployed_at = (Get-Date).ToUniversalTime().ToString("o") urls = @() } if (-not $SkipSmoke) { $nodeSmoke = @" process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; const baseUrl = "$BaseUrl"; const get = async (path) => { const response = await fetch(baseUrl + path); const buffer = Buffer.from(await response.arrayBuffer()); if (response.status !== 200) { throw new Error(path + " returned " + response.status); } return { path, status: response.status, bytes: buffer.length, text: buffer.toString("utf8") }; }; const manifestResponse = await get("/demo-manifest.json"); const manifest = JSON.parse(manifestResponse.text); const jobId = manifest.job_id; const paths = [ "/", "/demo-manifest.json", "/spec-programs/manifest.json", "/app/virtual-controller.html", "/test-results/abb120-spec/" + jobId + "/report.json", "/test-results/abb120-spec/" + jobId + "/report.html", "/test-results/virtual-controller/virtual-controller-desktop.png", "/test-results/virtual-controller/virtual-controller-mobile.png" ]; const urls = []; for (const path of paths) { const result = path === "/demo-manifest.json" ? manifestResponse : await get(path); urls.push({ url: baseUrl + path, status: result.status, bytes: result.bytes }); } const reportResponse = await get("/test-results/abb120-spec/" + jobId + "/report.json"); const report = JSON.parse(reportResponse.text); if (Number(manifest.program_count) !== 19) { throw new Error("Expected program_count=19, got " + manifest.program_count); } if (Number(report.summary.fail) !== 0) { throw new Error("Expected summary.fail=0, got " + report.summary.fail); } console.log(JSON.stringify({ job_id: jobId, program_count: Number(manifest.program_count), summary_fail: Number(report.summary.fail), urls })); "@ $nodeOutput = $nodeSmoke | node --input-type=module if ($LASTEXITCODE -ne 0) { throw "Smoke check failed" } $nodeSmokeResult = $nodeOutput | Select-Object -Last 1 | ConvertFrom-Json $smoke.job_id = $nodeSmokeResult.job_id $smoke.program_count = [int]$nodeSmokeResult.program_count $smoke.summary_fail = [int]$nodeSmokeResult.summary_fail $smoke.urls = @($nodeSmokeResult.urls) } $SmokePath = Join-Path $DeployRoot "kdl-olp-demo-$ReleaseId.deploy.json" $smoke | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $SmokePath -Encoding UTF8 $smoke | ConvertTo-Json -Depth 8