From 8e8445905fbdd30fb0a129aeee309eaf6dacdf64 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 27 May 2026 19:39:26 +0200 Subject: [PATCH] fix(release): stream cross-os served artifacts --- scripts/openclaw-cross-os-release-checks.ts | 21 +++++++++-- .../openclaw-cross-os-release-checks.test.ts | 37 +++++++++++++++++++ 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/scripts/openclaw-cross-os-release-checks.ts b/scripts/openclaw-cross-os-release-checks.ts index 160d8cf9d45..fe23f0af58f 100644 --- a/scripts/openclaw-cross-os-release-checks.ts +++ b/scripts/openclaw-cross-os-release-checks.ts @@ -6,6 +6,7 @@ import { spawn } from "node:child_process"; import { appendFileSync, chmodSync, + createReadStream, createWriteStream, existsSync, mkdirSync, @@ -3680,11 +3681,11 @@ async function runCommandInvocation(invocation, options) { }); } -async function startStaticFileServer(params) { +export async function startStaticFileServer(params) { mkdirSync(dirname(params.logPath), { recursive: true }); const logStream = createWriteStream(params.logPath, { flags: "a" }); const fileName = String(params.filePath.split(/[/\\]/u).at(-1) ?? "artifact"); - const fileBytes = readFileSync(params.filePath); + const fileStat = statSync(params.filePath); const server = createServer((request, response) => { logStream.write(`${new Date().toISOString()} ${request.method} ${request.url}\n`); if (request.url !== `/${fileName}`) { @@ -3694,8 +3695,20 @@ async function startStaticFileServer(params) { } response.statusCode = 200; response.setHeader("content-type", resolveStaticFileContentType(params.filePath)); - response.setHeader("content-length", String(fileBytes.length)); - response.end(fileBytes); + response.setHeader("content-length", String(fileStat.size)); + response.setHeader("connection", "close"); + const fileStream = createReadStream(params.filePath); + fileStream.once("error", (error) => { + logStream.write(`${new Date().toISOString()} static-file-read-error ${formatError(error)}\n`); + if (response.headersSent) { + response.destroy(error); + return; + } + response.removeHeader("content-length"); + response.statusCode = 500; + response.end("failed to read file"); + }); + fileStream.pipe(response); }); await new Promise((resolvePromise, rejectPromise) => { server.once("error", rejectPromise); diff --git a/test/scripts/openclaw-cross-os-release-checks.test.ts b/test/scripts/openclaw-cross-os-release-checks.test.ts index a86ef547127..a9fb6925680 100644 --- a/test/scripts/openclaw-cross-os-release-checks.test.ts +++ b/test/scripts/openclaw-cross-os-release-checks.test.ts @@ -65,6 +65,7 @@ import { resolveRequestedSuites, resolveRunnerMatrix, resolveStaticFileContentType, + startStaticFileServer, shouldExerciseManagedGatewayLifecycleAfterInstall, shouldRunPackagedUpgradeStatusProbe, shouldRunWindowsInstalledBrowserOverrideImportSmoke, @@ -659,6 +660,42 @@ describe("scripts/openclaw-cross-os-release-checks", () => { expect(resolveStaticFileContentType("openclaw-2026.4.14.tgz")).toBe("application/octet-stream"); }); + it("streams release artifacts from the static file server", async () => { + const dir = mkdtempSync(join(tmpdir(), "openclaw-cross-os-static-server-")); + const filePath = join(dir, "openclaw-2026.4.14.tgz"); + const logPath = join(dir, "server.log"); + let server: Awaited> | undefined; + + try { + const payload = Buffer.from(`artifact-head\n${"x".repeat(1024 * 1024)}\nartifact-tail`); + writeFileSync(filePath, payload); + + server = await startStaticFileServer({ filePath, logPath }); + const response = await fetch(server.url); + const body = Buffer.from(await response.arrayBuffer()); + + expect(response.status).toBe(200); + expect(response.headers.get("content-length")).toBe(String(payload.length)); + expect(response.headers.get("content-type")).toBe("application/octet-stream"); + expect(body.equals(payload)).toBe(true); + expect(readFileSync(logPath, "utf8")).toContain(`GET /${filePath.split(/[/\\]/u).at(-1)}`); + } finally { + await server?.close(); + rmSync(dir, { recursive: true, force: true }); + } + }); + + it("does not preload static release artifacts before serving them", () => { + const source = readFileSync("scripts/openclaw-cross-os-release-checks.ts", "utf8"); + const serverSource = source.slice( + source.indexOf("export async function startStaticFileServer"), + source.indexOf("export function resolveStaticFileContentType"), + ); + + expect(serverSource).toContain("createReadStream(params.filePath)"); + expect(serverSource).not.toContain("readFileSync(params.filePath)"); + }); + it("uses the published installer URLs for native installer lanes", () => { expect(resolvePublishedInstallerUrl("darwin")).toBe("https://openclaw.ai/install.sh"); expect(resolvePublishedInstallerUrl("linux")).toBe("https://openclaw.ai/install.sh");