fix(ci): package runtime resources in dist artifact

This commit is contained in:
clawsweeper
2026-08-01 16:49:43 +00:00
parent 3d27070eae
commit 1a32f89897
2 changed files with 127 additions and 93 deletions

View File

@@ -1,6 +1,6 @@
// Verifies installed packages can bootstrap the default OpenClaw workspace files.
import { execFileSync, spawn } from "node:child_process";
import { existsSync, mkdtempSync, mkdirSync, rmSync } from "node:fs";
import { cpSync, existsSync, mkdtempSync, mkdirSync, rmSync } from "node:fs";
import { createServer } from "node:net";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
@@ -25,6 +25,7 @@ export const DIST_RUNTIME_ARTIFACT_BASE_PATHS = [
"src/agents/templates",
"dist",
"dist-runtime",
"node_modules",
];
const DIST_RUNTIME_ARTIFACT_PACKAGE_DIST_PATHS = [
@@ -43,6 +44,37 @@ function packageArtifactPath(sourcePath) {
return sourcePath.replace(/^packages\//u, "node_modules/@openclaw/");
}
function copyDistRuntimeArtifactPath(
rootDir,
artifactRoot,
sourcePath,
destinationPath = sourcePath,
) {
const destination = join(artifactRoot, destinationPath);
mkdirSync(dirname(destination), { recursive: true });
cpSync(join(rootDir, sourcePath), destination, { dereference: true, recursive: true });
}
function stageDistRuntimeArtifact(rootDir, artifactRoot) {
const deploymentRoot = join(artifactRoot, "deployment");
execFileSync(
"pnpm",
["--ignore-scripts", "--filter", "openclaw", "deploy", "--legacy", "--prod", deploymentRoot],
{ cwd: rootDir, stdio: "inherit" },
);
copyDistRuntimeArtifactPath(deploymentRoot, artifactRoot, "node_modules");
rmSync(deploymentRoot, { force: true, recursive: true });
for (const sourcePath of DIST_RUNTIME_ARTIFACT_BASE_PATHS) {
if (sourcePath !== "node_modules") {
copyDistRuntimeArtifactPath(rootDir, artifactRoot, sourcePath);
}
}
for (const sourcePath of DIST_RUNTIME_ARTIFACT_PACKAGE_SOURCE_PATHS) {
copyDistRuntimeArtifactPath(rootDir, artifactRoot, sourcePath, packageArtifactPath(sourcePath));
}
}
// HEARTBEAT.md ships in the template pack for docs/doctor context but is no
// longer seeded into new workspaces; heartbeat context lives in cron scratch.
const REQUIRED_BOOTSTRAP_WORKSPACE_FILES = [
@@ -250,11 +282,11 @@ function validateDistRuntimeArtifactEntries(entries, expectedPaths) {
entry === "docs/reference/templates" ||
entry.startsWith("docs/reference/templates/") ||
entry === "node_modules" ||
entry === "node_modules/@openclaw"
entry.startsWith("node_modules/")
) {
return false;
}
return !/^node_modules\/@openclaw\/[^/]+\/(?:package\.json|dist(?:\/.*)?)$/u.test(entry);
return true;
});
if (unexpectedEntries.length > 0) {
throw new Error(
@@ -367,105 +399,102 @@ export async function buildAndSmokeDistRuntimeArtifact(params) {
const archivePath = resolve(params.archivePath);
const compressor = params.compressor ?? "zstdmt";
const artifactPaths = collectDistRuntimeArtifactPaths(rootDir);
const artifactRoot = mkdtempSync(join(tmpdir(), "openclaw-dist-runtime-artifact-"));
mkdirSync(dirname(archivePath), { recursive: true });
execFileSync(
"tar",
[
"--posix",
"-cf",
archivePath,
"--use-compress-program",
compressor,
// Workspace packages must resolve from the extracted artifact rather than
// the checkout that produced it; normal Node resolution then needs no hook.
"--transform=s,^packages/\\([^/\\]*\\)/,node_modules/@openclaw/\\1/,",
...DIST_RUNTIME_ARTIFACT_BASE_PATHS,
...DIST_RUNTIME_ARTIFACT_PACKAGE_SOURCE_PATHS,
],
{ cwd: rootDir, stdio: "inherit" },
);
const entries = listDistRuntimeArtifactEntries(archivePath, compressor);
validateDistRuntimeArtifactEntries(entries, artifactPaths);
const smokeRoot = mkdtempSync(join(tmpdir(), "openclaw-dist-runtime-artifact-smoke-"));
const packageRoot = join(smokeRoot, "package");
const homeDir = join(smokeRoot, "home");
const cwd = join(smokeRoot, "cwd");
mkdirSync(packageRoot, { recursive: true });
mkdirSync(homeDir, { recursive: true });
mkdirSync(cwd, { recursive: true });
const artifactEnvOverrides = {
OPENCLAW_CONFIG_PATH: join(homeDir, "openclaw.json"),
OPENCLAW_STATE_DIR: join(homeDir, "state"),
};
const smokeEnv = createWorkspaceBootstrapSmokeEnv(process.env, homeDir, artifactEnvOverrides);
let gateway;
let gatewayOutput = "";
try {
stageDistRuntimeArtifact(rootDir, artifactRoot);
execFileSync(
"tar",
["--use-compress-program", compressor, "-xf", archivePath, "-C", packageRoot],
{ stdio: "inherit" },
["--posix", "-cf", archivePath, "--use-compress-program", compressor, ...artifactPaths],
{ cwd: artifactRoot, stdio: "inherit" },
);
runInstalledWorkspaceBootstrapSmoke({
packageRoot,
envOverrides: artifactEnvOverrides,
});
const acpHelp = execFileSync(
process.execPath,
[join(packageRoot, "openclaw.mjs"), "acp", "--help"],
{
cwd,
encoding: "utf8",
env: smokeEnv,
maxBuffer: DIST_RUNTIME_ARTIFACT_MAX_OUTPUT_BYTES,
timeout: DIST_RUNTIME_ARTIFACT_SMOKE_TIMEOUT_MS,
},
);
if (!acpHelp.includes("Usage: openclaw acp")) {
throw new Error("extracted ACP startup did not return the expected help output");
}
const entries = listDistRuntimeArtifactEntries(archivePath, compressor);
validateDistRuntimeArtifactEntries(entries, artifactPaths);
const port = await reserveLoopbackPort();
gateway = spawn(
process.execPath,
[
join(packageRoot, "openclaw.mjs"),
"gateway",
"run",
"--allow-unconfigured",
"--auth",
"none",
"--bind",
"loopback",
"--port",
String(port),
],
{
cwd,
detached: process.platform !== "win32",
env: smokeEnv,
stdio: ["ignore", "pipe", "pipe"],
},
);
const appendGatewayOutput = (chunk) => {
gatewayOutput += chunk.toString();
if (Buffer.byteLength(gatewayOutput) > DIST_RUNTIME_ARTIFACT_MAX_OUTPUT_BYTES) {
gatewayOutput = gatewayOutput.slice(-DIST_RUNTIME_ARTIFACT_MAX_OUTPUT_BYTES);
}
const smokeRoot = mkdtempSync(join(tmpdir(), "openclaw-dist-runtime-artifact-smoke-"));
const packageRoot = join(smokeRoot, "package");
const homeDir = join(smokeRoot, "home");
const cwd = join(smokeRoot, "cwd");
mkdirSync(packageRoot, { recursive: true });
mkdirSync(homeDir, { recursive: true });
mkdirSync(cwd, { recursive: true });
const artifactEnvOverrides = {
OPENCLAW_CONFIG_PATH: join(homeDir, "openclaw.json"),
OPENCLAW_STATE_DIR: join(homeDir, "state"),
};
gateway.stdout.on("data", appendGatewayOutput);
gateway.stderr.on("data", appendGatewayOutput);
await waitForGatewayReadiness({ child: gateway, port, readOutput: () => gatewayOutput });
} finally {
if (gateway) {
await stopGatewaySmoke(gateway);
const smokeEnv = createWorkspaceBootstrapSmokeEnv(process.env, homeDir, artifactEnvOverrides);
const gatewayEnv = { ...smokeEnv };
delete gatewayEnv.OPENCLAW_DISABLE_BUNDLED_PLUGINS;
let gateway;
let gatewayOutput = "";
try {
execFileSync(
"tar",
["--use-compress-program", compressor, "-xf", archivePath, "-C", packageRoot],
{ stdio: "inherit" },
);
runInstalledWorkspaceBootstrapSmoke({
packageRoot,
envOverrides: artifactEnvOverrides,
});
const acpHelp = execFileSync(
process.execPath,
[join(packageRoot, "openclaw.mjs"), "acp", "--help"],
{
cwd,
encoding: "utf8",
env: smokeEnv,
maxBuffer: DIST_RUNTIME_ARTIFACT_MAX_OUTPUT_BYTES,
timeout: DIST_RUNTIME_ARTIFACT_SMOKE_TIMEOUT_MS,
},
);
if (!acpHelp.includes("Usage: openclaw acp")) {
throw new Error("extracted ACP startup did not return the expected help output");
}
const port = await reserveLoopbackPort();
gateway = spawn(
process.execPath,
[
join(packageRoot, "openclaw.mjs"),
"gateway",
"run",
"--allow-unconfigured",
"--auth",
"none",
"--bind",
"loopback",
"--port",
String(port),
],
{
cwd,
detached: process.platform !== "win32",
env: gatewayEnv,
stdio: ["ignore", "pipe", "pipe"],
},
);
const appendGatewayOutput = (chunk) => {
gatewayOutput += chunk.toString();
if (Buffer.byteLength(gatewayOutput) > DIST_RUNTIME_ARTIFACT_MAX_OUTPUT_BYTES) {
gatewayOutput = gatewayOutput.slice(-DIST_RUNTIME_ARTIFACT_MAX_OUTPUT_BYTES);
}
};
gateway.stdout.on("data", appendGatewayOutput);
gateway.stderr.on("data", appendGatewayOutput);
await waitForGatewayReadiness({ child: gateway, port, readOutput: () => gatewayOutput });
} finally {
if (gateway) {
await stopGatewaySmoke(gateway);
}
rmSync(smokeRoot, { force: true, recursive: true });
}
rmSync(smokeRoot, { force: true, recursive: true });
} finally {
rmSync(artifactRoot, { force: true, recursive: true });
}
return { archivePath, artifactPaths };

View File

@@ -4688,6 +4688,7 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}"
"TSDOWN_PACKAGE_OUTPUT_ROOTS",
'"packages/plugin-sdk/dist"',
'"node_modules/@openclaw/"',
'"node_modules"',
]) {
expect(artifactBuilder).toContain(requiredPath);
}
@@ -4695,6 +4696,10 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}"
expect(artifactBuilder).not.toContain("--import");
expect(artifactBuilder).not.toContain("dist-runtime-artifact-resolver-hook");
expect(artifactBuilder).toContain("node_modules/@openclaw");
expect(artifactBuilder).toContain(
'"--ignore-scripts", "--filter", "openclaw", "deploy", "--legacy", "--prod"',
);
expect(artifactBuilder).toContain("delete gatewayEnv.OPENCLAW_DISABLE_BUNDLED_PLUGINS");
expect(artifactBuilder).toContain('detached: process.platform !== "win32"');
expect(artifactBuilder).toContain('process.kill(processGroupId, "SIGKILL")');
expect(artifactBuilder).toContain('"acp", "--help"');