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,128 @@
|
||||
#!/usr/bin/env node
|
||||
// verify-online.mjs — Browser-level operational verification for a deployed service.
|
||||
// Used by Test-ServiceOnline.ps1. Exits 0 on success, non-zero on failure.
|
||||
//
|
||||
// Usage:
|
||||
// node verify-online.mjs <url> [--screenshot <path>] [--timeout <ms>] [--expect-title <regex>]
|
||||
//
|
||||
// Checks performed:
|
||||
// 1. Page navigates to status < 400 (HTTP layer via Playwright)
|
||||
// 2. No uncaught `pageerror` events (JS crashes)
|
||||
// 3. No failed main-frame network requests (DNS, 5xx on document, etc.)
|
||||
// 4. Optional: document.title matches a regex
|
||||
// 5. Saves a screenshot (PNG) when --screenshot is provided
|
||||
//
|
||||
// Output: a single JSON line on stdout with the result; human logs go to stderr.
|
||||
|
||||
import { chromium } from "playwright";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
if (args.length === 0) {
|
||||
console.error("Usage: verify-online.mjs <url> [--screenshot <path>] [--timeout <ms>] [--expect-title <regex>] [--no-headless]");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const url = args[0];
|
||||
const opts = {};
|
||||
for (let i = 1; i < args.length; i++) {
|
||||
const a = args[i];
|
||||
if (a === "--screenshot") opts.screenshot = args[++i];
|
||||
else if (a === "--timeout") opts.timeout = parseInt(args[++i], 10);
|
||||
else if (a === "--expect-title") opts.expectTitle = args[++i];
|
||||
else if (a === "--no-headless") opts.headless = false;
|
||||
}
|
||||
|
||||
const timeout = opts.timeout || 30000;
|
||||
const headless = opts.headless !== false;
|
||||
|
||||
const result = {
|
||||
url,
|
||||
ok: false,
|
||||
httpStatus: null,
|
||||
title: null,
|
||||
pageErrors: [],
|
||||
requestFailures: [],
|
||||
consoleErrors: [],
|
||||
screenshot: null,
|
||||
elapsedMs: null,
|
||||
};
|
||||
|
||||
const startedAt = Date.now();
|
||||
let browser;
|
||||
try {
|
||||
browser = await chromium.launch({ headless });
|
||||
const context = await browser.newContext({
|
||||
ignoreHTTPSErrors: true,
|
||||
viewport: { width: 1280, height: 720 },
|
||||
});
|
||||
const page = await context.newPage();
|
||||
|
||||
page.on("pageerror", (err) => {
|
||||
result.pageErrors.push(err.message);
|
||||
});
|
||||
page.on("requestfailed", (req) => {
|
||||
const u = req.url();
|
||||
// Ignore third-party analytics/CDN noise; surface only main-frame document/sub-resource
|
||||
// failures relevant to the app itself.
|
||||
const failure = req.failure();
|
||||
result.requestFailures.push({
|
||||
url: u,
|
||||
method: req.method(),
|
||||
errorText: failure ? failure.errorText : "unknown",
|
||||
resourceType: req.resourceType(),
|
||||
});
|
||||
});
|
||||
page.on("console", (msg) => {
|
||||
if (msg.type() === "error") {
|
||||
const txt = msg.text();
|
||||
// Suppress noisy third-party errors that don't affect operability.
|
||||
if (!/favicon|googletagmanager|google-analytics|doubleclick|sentry/i.test(txt)) {
|
||||
result.consoleErrors.push(txt);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const response = await page.goto(url, { waitUntil: "domcontentloaded", timeout });
|
||||
if (!response) {
|
||||
throw new Error("Navigation returned no response (blocked or timeout).");
|
||||
}
|
||||
result.httpStatus = response.status();
|
||||
result.title = await page.title();
|
||||
|
||||
if (opts.expectTitle) {
|
||||
const re = new RegExp(opts.expectTitle);
|
||||
if (!re.test(result.title || "")) {
|
||||
throw new Error(`Title "${result.title}" does not match /${opts.expectTitle}/`);
|
||||
}
|
||||
}
|
||||
|
||||
if (result.httpStatus >= 400) {
|
||||
throw new Error(`HTTP ${result.httpStatus} from ${url}`);
|
||||
}
|
||||
if (result.pageErrors.length > 0) {
|
||||
throw new Error(`Page threw ${result.pageErrors.length} uncaught error(s): ${result.pageErrors.slice(0, 3).join(" | ")}`);
|
||||
}
|
||||
|
||||
// Give SPA a moment to settle, then capture screenshot.
|
||||
await page.waitForTimeout(1500);
|
||||
if (opts.screenshot) {
|
||||
try {
|
||||
await page.screenshot({ path: opts.screenshot, fullPage: false });
|
||||
result.screenshot = opts.screenshot;
|
||||
} catch (e) {
|
||||
// Non-fatal: screenshot is best-effort.
|
||||
console.error(`[warn] screenshot failed: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
result.ok = true;
|
||||
} catch (err) {
|
||||
result.error = err.message;
|
||||
result.ok = false;
|
||||
} finally {
|
||||
if (browser) await browser.close().catch(() => {});
|
||||
result.elapsedMs = Date.now() - startedAt;
|
||||
console.log(JSON.stringify(result));
|
||||
}
|
||||
|
||||
process.exit(result.ok ? 0 : 1);
|
||||
Reference in New Issue
Block a user