Add detailed diagnostics to Dokploy deployment workflow
Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 4m2s
Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 4m2s
This commit is contained in:
176
scripts/dokploy-diagnostics.mjs
Normal file
176
scripts/dokploy-diagnostics.mjs
Normal file
@@ -0,0 +1,176 @@
|
||||
import fs from "node:fs";
|
||||
import dns from "node:dns/promises";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
export function redact(value, secrets = []) {
|
||||
let text = String(value);
|
||||
for (const secret of secrets.filter(Boolean)) {
|
||||
for (const variant of new Set([secret, encodeURIComponent(secret)]))
|
||||
text = text.split(variant).join("[REDACTED]");
|
||||
}
|
||||
return text
|
||||
.replace(/(https?:\/\/)[^\s/@]+:[^\s/@]+@/gi, "$1[REDACTED]@")
|
||||
.replace(
|
||||
/((?:authorization|x-api-key|cookie|set-cookie)\s*:\s*)[^\r\n]*/gi,
|
||||
"$1[REDACTED]",
|
||||
)
|
||||
.replace(/\b(Bearer|Basic)\s+[^\s"<>]+/gi, "$1 [REDACTED]")
|
||||
.replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, "");
|
||||
}
|
||||
|
||||
export function responseDetails(headers, body) {
|
||||
const lines = headers
|
||||
.split(/\r?\n/)
|
||||
.filter((line) =>
|
||||
/^(HTTP\/|content-type:|content-length:|server:|location:|cf-ray:|x-request-id:|retry-after:)/i.test(
|
||||
line,
|
||||
),
|
||||
);
|
||||
if (!body) return [...lines, "Response body: empty"];
|
||||
try {
|
||||
const data = JSON.parse(body);
|
||||
// Deployment lists may contain source/configuration. Never dump the payload.
|
||||
lines.push(
|
||||
`Response JSON shape: ${Array.isArray(data) ? `array (${data.length} items)` : data && typeof data === "object" ? Object.keys(data).slice(0, 20).join(", ") : typeof data}`,
|
||||
);
|
||||
for (const field of ["message", "error", "code"]) {
|
||||
const value = data?.[field];
|
||||
if (typeof value === "string" || typeof value === "number")
|
||||
lines.push(`${field}: ${String(value).slice(0, 2000)}`);
|
||||
else if (value && typeof value === "object") {
|
||||
for (const key of ["message", "code"])
|
||||
if (typeof value[key] === "string" || typeof value[key] === "number")
|
||||
lines.push(`${field}.${key}: ${String(value[key]).slice(0, 2000)}`);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
const title = body.match(/<title[^>]*>([^<]{0,300})<\/title>/i)?.[1];
|
||||
lines.push(
|
||||
title
|
||||
? `Response HTML title: ${title}`
|
||||
: `Response is not JSON (${Buffer.byteLength(body)} bytes); raw content withheld.`,
|
||||
);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
async function main([mode, ...args]) {
|
||||
if (mode === "environment") {
|
||||
const url = new URL(args[0]);
|
||||
console.error(
|
||||
`[Dokploy] Target: ${url.origin}; Node ${process.version}; runner ${process.platform}/${process.arch}`,
|
||||
);
|
||||
for (const name of [
|
||||
"HTTPS_PROXY",
|
||||
"https_proxy",
|
||||
"HTTP_PROXY",
|
||||
"http_proxy",
|
||||
"ALL_PROXY",
|
||||
"all_proxy",
|
||||
]) {
|
||||
if (!process.env[name]) continue;
|
||||
try {
|
||||
const proxy = new URL(process.env[name]);
|
||||
console.error(
|
||||
`[Dokploy] ${name}: ${proxy.protocol}//${proxy.host} (credentials omitted)`,
|
||||
);
|
||||
} catch {
|
||||
console.error(`[Dokploy] ${name}: configured (value withheld)`);
|
||||
}
|
||||
}
|
||||
console.error(
|
||||
`[Dokploy] NO_PROXY/no_proxy: ${process.env.NO_PROXY || process.env.no_proxy ? "configured" : "unset"}`,
|
||||
);
|
||||
for (const name of ["CURL_CA_BUNDLE", "SSL_CERT_FILE", "SSL_CERT_DIR"])
|
||||
console.error(
|
||||
`[Dokploy] ${name}: ${process.env[name] ? `configured; path exists=${fs.existsSync(process.env[name])}` : "default"}`,
|
||||
);
|
||||
let timer;
|
||||
try {
|
||||
const addresses = await Promise.race([
|
||||
dns.lookup(url.hostname, { all: true }),
|
||||
new Promise((_, reject) => {
|
||||
timer = setTimeout(
|
||||
() => reject(new Error("DNS lookup exceeded 5 seconds")),
|
||||
5000,
|
||||
);
|
||||
}),
|
||||
]);
|
||||
console.error(
|
||||
`[Dokploy] DNS ${url.hostname}: ${addresses.map((a) => `IPv${a.family} ${a.address}`).join(", ") || "no addresses"}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[Dokploy] DNS lookup failed: ${error.code ?? ""} ${error.message}`,
|
||||
);
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (mode !== "request")
|
||||
throw new Error("Expected environment or request mode");
|
||||
const [
|
||||
label,
|
||||
exitCode,
|
||||
metricsPath,
|
||||
errorPath,
|
||||
headersPath,
|
||||
bodyPath,
|
||||
authPath,
|
||||
] = args;
|
||||
const read = (file) =>
|
||||
fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
|
||||
const key = read(authPath)
|
||||
.replace(/^x-api-key:\s*/i, "")
|
||||
.trim();
|
||||
const safe = (text) =>
|
||||
redact(text, [key, process.env.DOKPLOY_APPLICATION_ID]);
|
||||
const metrics = read(metricsPath).trim();
|
||||
const status = metrics.split(/\r?\n/)[0] || "000";
|
||||
console.error(
|
||||
safe(
|
||||
`[Dokploy] ${label}: curl exit=${exitCode}\n${metrics || "No transfer metrics returned"}`,
|
||||
),
|
||||
);
|
||||
const stderr = read(errorPath).trim();
|
||||
if (stderr)
|
||||
console.error(
|
||||
safe(`[Dokploy] curl diagnostics:\n${stderr.slice(0, 8000)}`),
|
||||
);
|
||||
const hints = {
|
||||
5: "Proxy DNS failed. Check proxy configuration in the runner container.",
|
||||
6: "Target DNS failed. Check the domain records and runner container DNS.",
|
||||
7: "Connection failed. Check port 443, reverse proxy, firewall and runner routing.",
|
||||
28: "Request timed out. Transfer timings show whether DNS, connection or the server stalled.",
|
||||
35: "TLS handshake failed. Check the reverse proxy HTTPS listener and TLS configuration.",
|
||||
52: "The server closed the connection without an HTTP response.",
|
||||
60: "TLS certificate verification failed. Check the domain certificate, intermediate chain and runner CA bundle.",
|
||||
97: "Proxy handshake failed. Check the runner proxy configuration.",
|
||||
};
|
||||
if (hints[exitCode]) console.error(`[Dokploy] ${hints[exitCode]}`);
|
||||
if (exitCode !== "0" || !/^2\d\d$/.test(status)) {
|
||||
for (const line of responseDetails(read(headersPath), read(bodyPath)))
|
||||
console.error(safe(`[Dokploy] ${line}`));
|
||||
if (status === "000")
|
||||
console.error(
|
||||
"[Dokploy] HTTP 000 means no HTTP response was received; inspect the curl error and network timings above.",
|
||||
);
|
||||
if (/^3\d\d$/.test(status))
|
||||
console.error(
|
||||
"[Dokploy] Unexpected redirect. Check the Location header and proxy/auth routing; authenticated requests do not follow redirects.",
|
||||
);
|
||||
if (["401", "403"].includes(status))
|
||||
console.error(
|
||||
"[Dokploy] Access rejected by Dokploy or a proxy. Check API key permissions and proxy access rules.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
|
||||
main(process.argv.slice(2))
|
||||
.then(() => process.exit(0))
|
||||
.catch((error) => {
|
||||
console.error(`[Dokploy] Diagnostics failed: ${error.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user