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:
urieljareth
2026-07-08 00:42:12 -06:00
co-authored by Claude Opus 4.8
parent cd998ce6b0
commit e7d1c33cd5
22 changed files with 2935 additions and 0 deletions
+238
View File
@@ -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
}