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:
205
tests/dokploy-diagnostics.test.ts
Normal file
205
tests/dokploy-diagnostics.test.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { spawnSync, type SpawnSyncReturns } from "node:child_process";
|
||||
import { redact, responseDetails } from "../scripts/dokploy-diagnostics.mjs";
|
||||
|
||||
test("Dokploy diagnostics redact credentials and strip terminal controls", () => {
|
||||
const key = "secret/key?value";
|
||||
const message = redact(
|
||||
`Key ${key} encoded ${encodeURIComponent(key)}\nx-api-key: ${key}\nAuthorization: Bearer token\nhttps://user:password@proxy.test\n\u001b[31m`,
|
||||
[key],
|
||||
);
|
||||
for (const value of [
|
||||
key,
|
||||
encodeURIComponent(key),
|
||||
"Bearer token",
|
||||
"user:password",
|
||||
"\u001b",
|
||||
])
|
||||
assert.ok(!message.includes(value));
|
||||
assert.ok(message.includes("[REDACTED]"));
|
||||
});
|
||||
|
||||
test("API failure details include useful error fields without dumping deployments, configuration or cookies", () => {
|
||||
const result = responseDetails(
|
||||
"HTTP/2 403\r\nserver: cloudflare\r\ncf-ray: abc\r\nset-cookie: private\r\n",
|
||||
JSON.stringify({
|
||||
error: {
|
||||
message: "Permission denied",
|
||||
code: "FORBIDDEN",
|
||||
config: "private config",
|
||||
},
|
||||
source: "private source",
|
||||
}),
|
||||
).join("\n");
|
||||
assert.ok(result.includes("HTTP/2 403"));
|
||||
assert.ok(result.includes("Permission denied"));
|
||||
assert.ok(result.includes("cf-ray: abc"));
|
||||
for (const value of ["private config", "private source", "set-cookie"])
|
||||
assert.ok(!result.includes(value));
|
||||
const html = responseDetails(
|
||||
"content-type: text/html",
|
||||
"<title>Bad Gateway</title><body>Private proxy details</body>",
|
||||
).join("\n");
|
||||
assert.ok(html.includes("Bad Gateway"));
|
||||
assert.ok(!html.includes("Private proxy details"));
|
||||
});
|
||||
|
||||
test("HTTP 000 diagnostics preserve curl DNS/TLS errors, timings and exit codes while masking the key", () => {
|
||||
const directory = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), "minegeo-dokploy-diagnostics-"),
|
||||
);
|
||||
try {
|
||||
const file = (name: string, value: string) => {
|
||||
const target = path.join(directory, name);
|
||||
fs.writeFileSync(target, value);
|
||||
return target;
|
||||
};
|
||||
const key = "test-dokploy-secret-123";
|
||||
const metrics = file(
|
||||
"metrics",
|
||||
"000\nremote_ip= dns=0.002s connect=0s tls=0s total=0.003s\nssl_verify_result=20\n",
|
||||
);
|
||||
const headers = file("headers", "");
|
||||
const body = file("body", "");
|
||||
const auth = file("auth", `x-api-key: ${key}\n`);
|
||||
for (const [code, error, expected] of [
|
||||
[
|
||||
"6",
|
||||
"curl: (6) Could not resolve host: dokploy.minegeo.site",
|
||||
"Target DNS failed",
|
||||
],
|
||||
[
|
||||
"60",
|
||||
`curl: (60) SSL certificate problem: unable to get local issuer certificate ${key}`,
|
||||
"TLS certificate verification failed",
|
||||
],
|
||||
]) {
|
||||
const stderr = file("error", error);
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
"scripts/dokploy-diagnostics.mjs",
|
||||
"request",
|
||||
"API preflight",
|
||||
code,
|
||||
metrics,
|
||||
stderr,
|
||||
headers,
|
||||
body,
|
||||
auth,
|
||||
],
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.ok(result.stderr.includes(`curl exit=${code}`));
|
||||
assert.ok(result.stderr.includes(expected));
|
||||
assert.ok(result.stderr.includes("HTTP 000 means no HTTP response"));
|
||||
assert.ok(result.stderr.includes("dns=0.002s"));
|
||||
assert.ok(result.stderr.includes("Response body: empty"));
|
||||
assert.ok(!result.stderr.includes(key));
|
||||
assert.equal(result.stdout, "");
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(directory, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("deployment workflow has valid Bash syntax and captures failures without disabling certificate checks", () => {
|
||||
const workflow = fs
|
||||
.readFileSync(".gitea/workflows/deploy-dokploy.yml", "utf8")
|
||||
.replace(/\r\n/g, "\n");
|
||||
const script = workflow
|
||||
.split(" run: |\n")[1]
|
||||
?.split("\n")
|
||||
.map((line) => line.replace(/^ /, ""))
|
||||
.join("\n");
|
||||
assert.ok(script, "workflow script was not found");
|
||||
assert.ok(
|
||||
script.includes("readonly DOKPLOY_URL='https://dokploy.minegeo.site'"),
|
||||
);
|
||||
assert.ok(script.includes("curl_exit=$?"));
|
||||
assert.ok(script.includes("--silent --show-error"));
|
||||
assert.ok(!script.includes("2>/dev/null"));
|
||||
assert.ok(!script.includes("--insecure"));
|
||||
const bash =
|
||||
process.platform === "win32"
|
||||
? "C:/Program Files/Git/bin/bash.exe"
|
||||
: "/bin/bash";
|
||||
const result = spawnSync(bash, ["--noprofile", "--norc", "-n"], {
|
||||
input: script,
|
||||
encoding: "utf8",
|
||||
});
|
||||
assert.equal(result.status, 0, result.stderr || String(result.error));
|
||||
});
|
||||
|
||||
test("the workflow request wrapper fails on HTTP 000 and reports the original curl exit code", () => {
|
||||
const workflow = fs
|
||||
.readFileSync(".gitea/workflows/deploy-dokploy.yml", "utf8")
|
||||
.replace(/\r\n/g, "\n");
|
||||
const script = workflow
|
||||
.split(" run: |\n")[1]
|
||||
.split("\n")
|
||||
.map((line) => line.replace(/^ /, ""))
|
||||
.join("\n");
|
||||
const wrapper = script.match(/dokploy_request\(\) \{[\s\S]*?\n\}/)?.[0];
|
||||
assert.ok(wrapper);
|
||||
const directory = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), "minegeo-dokploy-wrapper-"),
|
||||
);
|
||||
const bash =
|
||||
process.platform === "win32"
|
||||
? "C:/Program Files/Git/bin/bash.exe"
|
||||
: "/bin/bash";
|
||||
try {
|
||||
fs.writeFileSync(
|
||||
path.join(directory, "auth"),
|
||||
"x-api-key: fake-secret-for-wrapper-test\n",
|
||||
);
|
||||
for (const [code, http, expected] of [
|
||||
[6, "000", 1],
|
||||
[60, "000", 1],
|
||||
[0, "403", 1],
|
||||
[0, "200", 0],
|
||||
] as const) {
|
||||
const result: SpawnSyncReturns<string> = spawnSync(
|
||||
bash,
|
||||
["--noprofile", "--norc", "-s"],
|
||||
{
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
FIXTURE_DIR: directory.replace(/\\/g, "/"),
|
||||
DOKPLOY_APPLICATION_ID: "test-app",
|
||||
},
|
||||
input: `set -eu
|
||||
auth_file="$FIXTURE_DIR/auth"
|
||||
metrics_file="$FIXTURE_DIR/metrics"
|
||||
error_file="$FIXTURE_DIR/error"
|
||||
headers_file="$FIXTURE_DIR/headers"
|
||||
${wrapper}
|
||||
curl() { printf '${http}\\ndns=0.001s total=0.002s\\n'; if [ ${code} -ne 0 ]; then printf 'curl: (${code}) simulated network failure\\n' >&2; fi; return ${code}; }
|
||||
dokploy_request 'Mock API preflight' "$FIXTURE_DIR/body" --get https://dokploy.minegeo.site/api/deployment.all
|
||||
`,
|
||||
},
|
||||
);
|
||||
assert.equal(
|
||||
result.status,
|
||||
expected,
|
||||
result.stderr || String(result.error),
|
||||
);
|
||||
assert.ok(result.stderr.includes(`curl exit=${code}`));
|
||||
assert.ok(result.stderr.includes(http));
|
||||
assert.ok(!result.stderr.includes("fake-secret-for-wrapper-test"));
|
||||
if (code)
|
||||
assert.ok(
|
||||
result.stderr.includes(`curl: (${code}) simulated network failure`),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(directory, { recursive: true });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user