Skip to content

Commit d594d04

Browse files
committed
fix(cli): probe the rendered compose network
1 parent 86bdaec commit d594d04

2 files changed

Lines changed: 74 additions & 20 deletions

File tree

packages/cli/__tests__/cli-check.test.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { describe, expect, it } from "vitest";
2-
import { buildProbeArgs, DEEP_PROBES, PROBE_IMAGE } from "../src/commands/check";
2+
import {
3+
buildProbeArgs,
4+
composeNetworkName,
5+
DEEP_PROBES,
6+
PROBE_IMAGE,
7+
probeFailureDetail,
8+
} from "../src/commands/check";
39

410
describe("buildProbeArgs", () => {
511
it("builds args for a standalone `docker run` curl container on the compose network", () => {
@@ -34,6 +40,41 @@ describe("buildProbeArgs", () => {
3440
});
3541
});
3642

43+
describe("composeNetworkName", () => {
44+
it("uses the explicit network name from this checkout's compose config", () => {
45+
expect(
46+
composeNetworkName(
47+
JSON.stringify({
48+
name: "docker",
49+
networks: { openmapx: { name: "production_openmapx" } },
50+
}),
51+
),
52+
).toBe("production_openmapx");
53+
});
54+
55+
it("falls back to the project-derived network name", () => {
56+
expect(composeNetworkName(JSON.stringify({ name: "docker", networks: {} }))).toBe(
57+
"docker_openmapx",
58+
);
59+
expect(composeNetworkName("not json")).toBeNull();
60+
});
61+
});
62+
63+
describe("probeFailureDetail", () => {
64+
it("keeps the actionable Docker error instead of the generic help footer", () => {
65+
expect(
66+
probeFailureDetail(
67+
"docker: Error response from daemon: network deploy_openmapx not found.\n\nRun 'docker run --help' for more information",
68+
125,
69+
),
70+
).toBe("docker: Error response from daemon: network deploy_openmapx not found.");
71+
});
72+
73+
it("falls back to the exit code when stderr is empty", () => {
74+
expect(probeFailureDetail("", 125)).toBe("exit 125");
75+
});
76+
});
77+
3778
describe("DEEP_PROBES", () => {
3879
it("bounds the overpass query with an explicit timeout", () => {
3980
// A timeout-less Overpass query reaped under load can orphan its

packages/cli/src/commands/check.ts

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,31 @@ interface ProbeResult {
6060
detail: string;
6161
}
6262

63+
export function probeFailureDetail(stderr: string, exitCode: number): string {
64+
const lines = stderr
65+
.split("\n")
66+
.map((line) => line.trim())
67+
.filter((line) => line && !/^Run 'docker run --help' for more information\.?$/.test(line));
68+
return lines.at(-1) ?? `exit ${exitCode}`;
69+
}
70+
71+
export function composeNetworkName(configText: string): string | null {
72+
try {
73+
const config = JSON.parse(configText) as {
74+
name?: unknown;
75+
networks?: Record<string, { name?: unknown } | null>;
76+
};
77+
const explicitName = config.networks?.openmapx?.name;
78+
if (typeof explicitName === "string" && explicitName.trim()) return explicitName.trim();
79+
if (typeof config.name === "string" && config.name.trim()) {
80+
return `${config.name.trim()}_openmapx`;
81+
}
82+
} catch {
83+
// Fall back to the conventional reference-deployment network below.
84+
}
85+
return null;
86+
}
87+
6388
/**
6489
* Probe image: a small, pinned curl container. curl's ENTRYPOINT is `curl`, so
6590
* everything after the image name is curl flags. `-f` turns non-2xx responses
@@ -87,12 +112,7 @@ async function runDeepProbe(serviceId: string, network: string): Promise<ProbeRe
87112
const result = await dockerRun(buildProbeArgs(network, url));
88113

89114
if (result.exitCode !== 0) {
90-
const stderr =
91-
result.stderr
92-
.split("\n")
93-
.filter((line) => line.trim())
94-
.slice(-1)[0] ?? "";
95-
return { status: "fail", detail: stderr.trim() || `exit ${result.exitCode}` };
115+
return { status: "fail", detail: probeFailureDetail(result.stderr, result.exitCode) };
96116
}
97117
if (probe.expect && !result.stdout.toLowerCase().includes(probe.expect.toLowerCase())) {
98118
return { status: "fail", detail: `response missing "${probe.expect}"` };
@@ -101,20 +121,13 @@ async function runDeepProbe(serviceId: string, network: string): Promise<ProbeRe
101121
}
102122

103123
async function detectComposeNetwork(): Promise<string> {
104-
// docker-compose names networks `<project>_<name>`. Our compose declares one
105-
// network called `openmapx`, so the project prefix is the only variable.
106-
const result = await dockerCompose(["config", "--services"]);
124+
// Read the network name from this checkout's rendered config. `compose ls`
125+
// lists every project on the host and its first row may belong to an
126+
// unrelated deployment (for example the docs stack).
127+
const result = await dockerCompose(["config", "--format", "json"]);
107128
if (result.exitCode === 0) {
108-
const inspect = await dockerCompose(["ls", "--format", "json"]);
109-
if (inspect.exitCode === 0 && inspect.stdout.trim()) {
110-
try {
111-
const rows = JSON.parse(inspect.stdout) as Array<{ Name?: string }>;
112-
const project = rows.find((r) => r.Name)?.Name;
113-
if (project) return `${project}_openmapx`;
114-
} catch {
115-
// fall through
116-
}
117-
}
129+
const name = composeNetworkName(result.stdout);
130+
if (name) return name;
118131
}
119132
return "docker_openmapx";
120133
}

0 commit comments

Comments
 (0)