deploy_skill: soporte para Coolify v4.1.2 (API /applications 404) + flujo UI Playwright
Aprendido desplegando cotizador (Next.js+FastAPI+Postgres, Docker Compose, repo privado) en coolify.urieljareth.org (v4.1.2): - Documenta que /applications/* devuelve 404 en esta instancia y que configurar apps git build-from-source solo es posible por la UI web. - Nuevos scripts scripts/coolify-ui/: coolify-login.mjs (guarda storageState) y Configure-CoolifyComposeApp.mjs (build pack -> Docker Compose, Reload Compose File, env vars por Developer view, dominios por servicio). - references/coolify-4.1.2-notes.md: mapa de API funcional/404, gotchas (BOM UTF-8 rompe el parser YAML de Coolify; Reload Compose File obligatorio; pin de dominios por servicio o 503; no encolar deploys concurrentes; PowerShell [IO.File]::ReadAllText para payloads de llaves). - env.local.template.ps1: COOLIFY_EMAIL/COOLIFY_PASSWORD para la UI. - SKILL.md/TOOLS.md: seccion "instance reality" y flujo UI paso a paso. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
cd998ce6b0
commit
e7d1c33cd5
@@ -0,0 +1,238 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Operational verification: confirm a deployed service actually serves its
|
||||
page with HTTP 200 and no client-side crashes.
|
||||
|
||||
.DESCRIPTION
|
||||
The final "is it live and working?" gate after a Coolify deploy. Runs two
|
||||
layers of checks and FAILS (non-zero exit) if either is unhealthy:
|
||||
|
||||
Layer 1 - HTTP layer (curl.exe)
|
||||
HEAD or GET against the FQDN, follow redirects, expect 2xx.
|
||||
Cheap, no browser. Catches Traefik 502, wrong ports_exposes, TLS
|
||||
failure, missing FQDN route.
|
||||
|
||||
Layer 2 - Browser layer (Playwright via Node, verify-online.mjs)
|
||||
Real Chromium navigation. Catches client-side JS crashes
|
||||
(pageerror), failed main-frame requests, runtime exceptions, and
|
||||
verifies the page actually rendered (not a blank/error page).
|
||||
Saves a PNG screenshot for evidence.
|
||||
|
||||
This script complements Test-PostDeploy.ps1 (which inspects Coolify state,
|
||||
container status, proxy logs, sibling DNS). Run BOTH:
|
||||
- Test-PostDeploy.ps1 -> "did Coolify ship it correctly?"
|
||||
- Test-ServiceOnline.ps1 -> "does the user actually get a working page?"
|
||||
|
||||
.PARAMETER Fqdn
|
||||
Full public URL, e.g. https://cotizador.urieljareth.org. Required.
|
||||
|
||||
.PARAMETER Path
|
||||
Path appended to the FQDN. Default: "/".
|
||||
Use a non-authenticated landing route (e.g. /, /login, /health) - Playwright
|
||||
will follow redirects, so / often works even when the app forces login.
|
||||
|
||||
.PARAMETER ExpectedStatusCode
|
||||
HTTP status code expected from Layer 1. Default: 200. Use 302 if the
|
||||
landing path legitimately redirects (e.g. / -> /login).
|
||||
|
||||
.PARAMETER ExpectTitle
|
||||
Optional regex the document.title must match. Example: "Cotizador|Login".
|
||||
|
||||
.PARAMETER Screenshot
|
||||
Optional PNG path for the Playwright screenshot. Defaults to
|
||||
"$env:TEMP\coolify-<host>-<timestamp>.png".
|
||||
|
||||
.PARAMETER SkipBrowser
|
||||
Skip Layer 2 (Playwright). Use only on headless/CI environments without
|
||||
a display, or when Chromium is unavailable.
|
||||
|
||||
.PARAMETER TimeoutMs
|
||||
Per-attempt timeout (curl and Playwright). Default: 30000.
|
||||
|
||||
.PARAMETER Retries
|
||||
Number of times to retry the FULL check with a 10s gap. Coolify + Traefik
|
||||
DNS challenge + warm-up can take a minute or two on first deploy.
|
||||
Default: 6 (so up to ~1 minute of retries).
|
||||
|
||||
.EXAMPLE
|
||||
.\Test-ServiceOnline.ps1 -Fqdn https://cotizador.urieljareth.org `
|
||||
-ExpectTitle "Cotizador|Login" -Retries 6
|
||||
|
||||
.EXAMPLE
|
||||
.\Test-ServiceOnline.ps1 -Fqdn https://myapp.urieljareth.org -SkipBrowser
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Fqdn,
|
||||
|
||||
[string]$Path = "/",
|
||||
|
||||
[int]$ExpectedStatusCode = 200,
|
||||
|
||||
[string]$ExpectTitle,
|
||||
|
||||
[string]$Screenshot,
|
||||
|
||||
[switch]$SkipBrowser,
|
||||
|
||||
[int]$TimeoutMs = 30000,
|
||||
|
||||
[int]$Retries = 6
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$repoRoot = (Resolve-Path (Join-Path $here "..\..")).Path
|
||||
$verifyNode = Join-Path $here "verify-online.mjs"
|
||||
$nodeModulesRoot = Join-Path $repoRoot "node_modules"
|
||||
|
||||
function Write-Section($t) { Write-Host "`n=== $t ===" -ForegroundColor Cyan }
|
||||
|
||||
function Get-FullUrl {
|
||||
param([string]$Base, [string]$P)
|
||||
$b = $Base.TrimEnd("/")
|
||||
if ($P -eq "/") { return $b + "/" }
|
||||
if (-not $P.StartsWith("/")) { $P = "/" + $P }
|
||||
return $b + $P
|
||||
}
|
||||
|
||||
function Test-HttpLayer {
|
||||
param([string]$Url, [int]$Expected, [int]$TimeoutMs)
|
||||
Write-Section "Layer 1 - HTTP (curl) -> $Url"
|
||||
$tmp = [System.IO.Path]::GetTempFileName()
|
||||
try {
|
||||
# -L follow redirects, -o NUL discard body, -w write status to tmp, --max-time total budget.
|
||||
$codeField = "%{http_code}"
|
||||
& curl.exe -sS -L -k -o NUL `
|
||||
-w $codeField `
|
||||
--max-time ([math]::Floor($TimeoutMs / 1000)) `
|
||||
$Url 2>$null > $tmp
|
||||
$raw = (Get-Content -LiteralPath $tmp -Raw -ErrorAction SilentlyContinue)
|
||||
$status = -1
|
||||
if ([int]::TryParse(($raw -replace '\s',''), [ref]$status)) {
|
||||
Write-Host " HTTP status: $status" -ForegroundColor $(if ($status -eq $Expected -or ($status -ge 200 -and $status -lt 400)) { 'Green' } else { 'Red' })
|
||||
return $status
|
||||
}
|
||||
Write-Host " Could not parse HTTP status. Raw: '$raw'" -ForegroundColor Red
|
||||
return -1
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
function Test-BrowserLayer {
|
||||
param(
|
||||
[string]$Url,
|
||||
[string]$TitleRegex,
|
||||
[string]$ScreenshotPath,
|
||||
[int]$TimeoutMs
|
||||
)
|
||||
Write-Section "Layer 2 - Browser (Playwright) -> $Url"
|
||||
if (-not (Test-Path -LiteralPath $nodeModulesRoot)) {
|
||||
Write-Host " node_modules not found at $nodeModulesRoot - run 'npm install' in the manager repo." -ForegroundColor Yellow
|
||||
Write-Host " Skipping browser layer (treat as inconclusive)." -ForegroundColor Yellow
|
||||
return $true # Do not fail the whole pipeline if browser deps are missing.
|
||||
}
|
||||
|
||||
if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
|
||||
Write-Host " node not in PATH. Skipping browser layer." -ForegroundColor Yellow
|
||||
return $true
|
||||
}
|
||||
|
||||
$nodeArgs = @($verifyNode, $Url, "--timeout", "$TimeoutMs")
|
||||
if ($TitleRegex) { $nodeArgs += @("--expect-title", $TitleRegex) }
|
||||
if ($ScreenshotPath) { $nodeArgs += @("--screenshot", $ScreenshotPath) }
|
||||
|
||||
# Run from the manager repo root so `require('playwright')` resolves.
|
||||
Push-Location -LiteralPath $repoRoot
|
||||
$jsonOut = & node @nodeArgs 2>&1
|
||||
$exitCode = $LASTEXITCODE
|
||||
Pop-Location
|
||||
|
||||
# The last non-empty line that parses as JSON is the result from verify-online.mjs.
|
||||
$parsed = $null
|
||||
foreach ($line in ($jsonOut -split "`n")) {
|
||||
$t = $line.Trim()
|
||||
if (-not $t) { continue }
|
||||
try { $parsed = $t | ConvertFrom-Json } catch { }
|
||||
}
|
||||
|
||||
if ($null -eq $parsed) {
|
||||
Write-Host " No JSON result from Node script (exit=$exitCode)." -ForegroundColor Red
|
||||
Write-Host " Raw output: $jsonOut" -ForegroundColor DarkGray
|
||||
return $false
|
||||
}
|
||||
|
||||
Write-Host " ok : $($parsed.ok)" -ForegroundColor $(if ($parsed.ok) { 'Green' } else { 'Red' })
|
||||
Write-Host " httpStatus : $($parsed.httpStatus)"
|
||||
Write-Host " title : $($parsed.title)"
|
||||
Write-Host " elapsedMs : $($parsed.elapsedMs)"
|
||||
if ($parsed.pageErrors -and $parsed.pageErrors.Count -gt 0) {
|
||||
Write-Host " pageErrors : $($parsed.pageErrors.Count)" -ForegroundColor Yellow
|
||||
$parsed.pageErrors | Select-Object -First 3 | ForEach-Object { Write-Host " - $_" -ForegroundColor DarkYellow }
|
||||
}
|
||||
if ($parsed.requestFailures -and $parsed.requestFailures.Count -gt 0) {
|
||||
Write-Host " reqFailures : $($parsed.requestFailures.Count)" -ForegroundColor Yellow
|
||||
$parsed.requestFailures | Select-Object -First 3 | ForEach-Object { Write-Host " - $($_.method) $($_.url) [$($_.errorText)]" -ForegroundColor DarkYellow }
|
||||
}
|
||||
if ($parsed.consoleErrors -and $parsed.consoleErrors.Count -gt 0) {
|
||||
Write-Host " consoleErrors: $($parsed.consoleErrors.Count)" -ForegroundColor Yellow
|
||||
}
|
||||
if ($parsed.error) {
|
||||
Write-Host " error : $($parsed.error)" -ForegroundColor Red
|
||||
}
|
||||
if ($parsed.screenshot) {
|
||||
Write-Host " screenshot : $($parsed.screenshot)" -ForegroundColor DarkGray
|
||||
}
|
||||
return [bool]$parsed.ok
|
||||
}
|
||||
|
||||
$url = Get-FullUrl -Base $Fqdn -P $Path
|
||||
|
||||
if (-not $Screenshot) {
|
||||
$host_ = ([System.Uri]$Fqdn).Host
|
||||
$ts = (Get-Date -Format "yyyyMMdd-HHmmss")
|
||||
$Screenshot = Join-Path $env:TEMP "coolify-$host_-$ts.png"
|
||||
}
|
||||
|
||||
$attempt = 0
|
||||
$lastResult = $false
|
||||
while ($attempt -lt $Retries) {
|
||||
$attempt++
|
||||
Write-Host "`n>>> Attempt $attempt/$Retries" -ForegroundColor Magenta
|
||||
|
||||
$httpStatus = Test-HttpLayer -Url $url -Expected $ExpectedStatusCode -TimeoutMs $TimeoutMs
|
||||
$httpOk = ($httpStatus -ge 200 -and $httpStatus -lt 400)
|
||||
if (-not $httpOk) {
|
||||
Write-Host " HTTP layer not ready (status=$httpStatus)." -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
$browserOk = $true
|
||||
if ($httpOk -and -not $SkipBrowser) {
|
||||
$browserOk = Test-BrowserLayer -Url $url -TitleRegex $ExpectTitle -ScreenshotPath $Screenshot -TimeoutMs $TimeoutMs
|
||||
} elseif ($SkipBrowser) {
|
||||
Write-Host " Browser layer skipped (-SkipBrowser)." -ForegroundColor DarkGray
|
||||
}
|
||||
|
||||
if ($httpOk -and $browserOk) {
|
||||
$lastResult = $true
|
||||
break
|
||||
}
|
||||
|
||||
if ($attempt -lt $Retries) {
|
||||
Write-Host " Waiting 10s before retry..." -ForegroundColor DarkGray
|
||||
Start-Sleep -Seconds 10
|
||||
}
|
||||
}
|
||||
|
||||
Write-Section "Verdict"
|
||||
if ($lastResult) {
|
||||
Write-Host " SERVICE ONLINE: $url returned healthy HTTP and a clean page render." -ForegroundColor Green
|
||||
Write-Host " Screenshot: $Screenshot" -ForegroundColor DarkGray
|
||||
exit 0
|
||||
} else {
|
||||
Write-Host " SERVICE NOT VERIFIED ONLINE after $attempt attempt(s)." -ForegroundColor Red
|
||||
Write-Host " Check: Coolify deploy logs, ports_exposes, FQDN route, TLS DNS challenge, app boot." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
Reference in New Issue
Block a user