mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-06 15:18:58 +00:00
fix(ci): harden full release live checks
This commit is contained in:
@@ -130,7 +130,8 @@ RUN printf 'packages:\n - .\n - ui\n' > /tmp/pnpm-workspace.runtime.yaml && \
|
||||
cp /tmp/pnpm-workspace.runtime.yaml pnpm-workspace.yaml && \
|
||||
CI=true NPM_CONFIG_FROZEN_LOCKFILE=false pnpm prune --prod && \
|
||||
node scripts/postinstall-bundled-plugins.mjs && \
|
||||
find dist -type f \( -name '*.d.ts' -o -name '*.d.mts' -o -name '*.d.cts' -o -name '*.map' \) -delete
|
||||
find dist -type f \( -name '*.d.ts' -o -name '*.d.mts' -o -name '*.d.cts' -o -name '*.map' \) -delete && \
|
||||
node scripts/check-package-dist-imports.mjs /app
|
||||
|
||||
# ── Runtime base image ──────────────────────────────────────────
|
||||
FROM ${OPENCLAW_NODE_BOOKWORM_SLIM_IMAGE} AS base-runtime
|
||||
|
||||
@@ -18,13 +18,7 @@ import {
|
||||
getShellEnvAppliedKeys,
|
||||
isLiveProfileKeyModeEnabled,
|
||||
isLiveTestEnabled,
|
||||
isModelNotFoundErrorMessage,
|
||||
isTruthyEnvValue,
|
||||
isAuthErrorMessage,
|
||||
isBillingErrorMessage,
|
||||
isOverloadedErrorMessage,
|
||||
isServerErrorMessage,
|
||||
isTimeoutErrorMessage,
|
||||
normalizeVideoGenerationDuration,
|
||||
parseCsvFilter,
|
||||
parseProviderModelMap,
|
||||
@@ -42,6 +36,7 @@ import type {
|
||||
VideoGenerationRequest,
|
||||
} from "openclaw/plugin-sdk/test-env";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveLiveVideoSkipReason } from "../test/helpers/media-generation/live-video-skip-reason.js";
|
||||
import alibabaPlugin from "./alibaba/index.js";
|
||||
import byteplusPlugin from "./byteplus/index.js";
|
||||
import deepinfraPlugin from "./deepinfra/index.js";
|
||||
@@ -77,7 +72,7 @@ const LIVE_VIDEO_OPERATION_TIMEOUT_MS = readPositiveIntegerEnv(
|
||||
const LIVE_VIDEO_TEST_TIMEOUT_MS =
|
||||
(RUN_FULL_VIDEO_MODES ? 3 : 1) * LIVE_VIDEO_OPERATION_TIMEOUT_MS + 30_000;
|
||||
const LIVE_VIDEO_SMOKE_PROMPT =
|
||||
"A one-second low-motion video of a lobster walking across wet sand, no text.";
|
||||
"A one-second low-motion video of a blue cube sliding across a clean studio floor.";
|
||||
|
||||
type LiveProviderCase = {
|
||||
plugin: Parameters<typeof registerProviderPlugin>[0]["plugin"];
|
||||
@@ -230,39 +225,6 @@ function buildLiveCapabilityOverrides(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function resolveLiveVideoSkipReason(message: string): string | null {
|
||||
if (isAuthErrorMessage(message)) {
|
||||
return "auth drift";
|
||||
}
|
||||
if (isModelNotFoundErrorMessage(message)) {
|
||||
return "model drift";
|
||||
}
|
||||
if (isBillingErrorMessage(message)) {
|
||||
return "billing drift";
|
||||
}
|
||||
if (
|
||||
isTimeoutErrorMessage(message) ||
|
||||
/did not finish in time/i.test(message) ||
|
||||
/last status:\s*in_progress/i.test(message)
|
||||
) {
|
||||
return "provider timeout";
|
||||
}
|
||||
if (isOverloadedErrorMessage(message) || isServerErrorMessage(message)) {
|
||||
return "provider outage";
|
||||
}
|
||||
if (
|
||||
/HTTP\s+404/i.test(message) &&
|
||||
/Invalid URL/i.test(message) &&
|
||||
/\/platform\/video_gen/i.test(message)
|
||||
) {
|
||||
return "provider endpoint drift";
|
||||
}
|
||||
if (/access denied|not authorized|not enabled|permission denied/i.test(message)) {
|
||||
return "provider/model drift";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function runLiveVideoAttempt(params: {
|
||||
authLabel: string;
|
||||
attempted: string[];
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import { LOCAL_BUILD_METADATA_DIST_PATHS } from "./lib/local-build-metadata-paths.mjs";
|
||||
import { collectPackageDistImportErrors } from "./lib/package-dist-imports.mjs";
|
||||
|
||||
function usage() {
|
||||
return "Usage: node scripts/check-openclaw-package-tarball.mjs <openclaw.tgz>";
|
||||
@@ -195,6 +196,13 @@ if (entrySet.has("dist/postinstall-inventory.json")) {
|
||||
}
|
||||
}
|
||||
|
||||
errors.push(
|
||||
...collectPackageDistImportErrors({
|
||||
files: normalized,
|
||||
readText: readTarEntry,
|
||||
}),
|
||||
);
|
||||
|
||||
if (errors.length > 0) {
|
||||
fail(`OpenClaw package tarball integrity failed:\n${errors.join("\n")}`);
|
||||
}
|
||||
|
||||
55
scripts/check-package-dist-imports.mjs
Normal file
55
scripts/check-package-dist-imports.mjs
Normal file
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { collectPackageDistImportErrors } from "./lib/package-dist-imports.mjs";
|
||||
|
||||
function usage() {
|
||||
return "Usage: node scripts/check-package-dist-imports.mjs [package-root]";
|
||||
}
|
||||
|
||||
function fail(message) {
|
||||
console.error(message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const packageRoot = path.resolve(process.argv[2] ?? process.cwd());
|
||||
if (process.argv.length > 3) {
|
||||
fail(usage());
|
||||
}
|
||||
|
||||
const distRoot = path.join(packageRoot, "dist");
|
||||
if (!fs.existsSync(distRoot)) {
|
||||
fail(`missing dist directory: ${distRoot}`);
|
||||
}
|
||||
|
||||
function collectFiles(rootDir) {
|
||||
const pending = [rootDir];
|
||||
const files = [];
|
||||
while (pending.length > 0) {
|
||||
const dir = pending.pop();
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const entryPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
pending.push(entryPath);
|
||||
continue;
|
||||
}
|
||||
if (entry.isFile()) {
|
||||
files.push(path.relative(packageRoot, entryPath).replace(/\\/gu, "/"));
|
||||
}
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
const errors = collectPackageDistImportErrors({
|
||||
files: collectFiles(distRoot),
|
||||
readText(relativePath) {
|
||||
return fs.readFileSync(path.join(packageRoot, relativePath), "utf8");
|
||||
},
|
||||
});
|
||||
|
||||
if (errors.length > 0) {
|
||||
fail(`OpenClaw package dist import closure failed:\n${errors.join("\n")}`);
|
||||
}
|
||||
|
||||
console.log("OpenClaw package dist import closure passed.");
|
||||
131
scripts/lib/package-dist-imports.mjs
Normal file
131
scripts/lib/package-dist-imports.mjs
Normal file
@@ -0,0 +1,131 @@
|
||||
import path from "node:path";
|
||||
|
||||
const JS_DIST_FILE_RE = /^dist\/.*\.(?:cjs|js|mjs)$/u;
|
||||
|
||||
function normalizePackagePath(value) {
|
||||
return value.replace(/\\/gu, "/").replace(/^package\//u, "");
|
||||
}
|
||||
|
||||
function stripSpecifierSuffix(value) {
|
||||
return value.replace(/[?#].*$/u, "");
|
||||
}
|
||||
|
||||
function resolveDistImportPath(importerPath, specifier) {
|
||||
if (!specifier.startsWith(".")) {
|
||||
return null;
|
||||
}
|
||||
const stripped = stripSpecifierSuffix(specifier);
|
||||
if (!stripped) {
|
||||
return null;
|
||||
}
|
||||
return path.posix.normalize(path.posix.join(path.posix.dirname(importerPath), stripped));
|
||||
}
|
||||
|
||||
function findStatementStart(source, index) {
|
||||
return (
|
||||
Math.max(
|
||||
source.lastIndexOf(";", index),
|
||||
source.lastIndexOf("{", index),
|
||||
source.lastIndexOf("}", index),
|
||||
source.lastIndexOf("\n", index),
|
||||
source.lastIndexOf("\r", index),
|
||||
) + 1
|
||||
);
|
||||
}
|
||||
|
||||
function isImportSpecifierContext(source, index) {
|
||||
const dynamicPrefix = source.slice(Math.max(0, index - 32), index);
|
||||
if (/\bimport\s*\(\s*$/u.test(dynamicPrefix)) {
|
||||
return true;
|
||||
}
|
||||
const statementPrefix = source.slice(findStatementStart(source, index), index).trimStart();
|
||||
return (
|
||||
/^(?:import|export)\b[\s\S]*\bfrom\s*$/u.test(statementPrefix) ||
|
||||
/^import\s*$/u.test(statementPrefix)
|
||||
);
|
||||
}
|
||||
|
||||
function collectImportSpecifiers(source) {
|
||||
const specifiers = [];
|
||||
let inBlockComment = false;
|
||||
let inLineComment = false;
|
||||
for (let index = 0; index < source.length; index += 1) {
|
||||
if (inBlockComment) {
|
||||
if (source[index] === "*" && source[index + 1] === "/") {
|
||||
inBlockComment = false;
|
||||
index += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (inLineComment) {
|
||||
if (source[index] === "\n" || source[index] === "\r") {
|
||||
inLineComment = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (source[index] === "/" && source[index + 1] === "*") {
|
||||
inBlockComment = true;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (source[index] === "/" && source[index + 1] === "/") {
|
||||
inLineComment = true;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const quote = source[index];
|
||||
if (quote !== '"' && quote !== "'") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let cursor = index + 1;
|
||||
let value = "";
|
||||
while (cursor < source.length) {
|
||||
const char = source[cursor];
|
||||
if (char === "\\") {
|
||||
value += source.slice(cursor, cursor + 2);
|
||||
cursor += 2;
|
||||
continue;
|
||||
}
|
||||
if (char === quote) {
|
||||
break;
|
||||
}
|
||||
value += char;
|
||||
cursor += 1;
|
||||
}
|
||||
if (cursor >= source.length) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (value.startsWith(".")) {
|
||||
if (isImportSpecifierContext(source, index)) {
|
||||
specifiers.push(value);
|
||||
}
|
||||
}
|
||||
index = cursor;
|
||||
}
|
||||
return specifiers;
|
||||
}
|
||||
|
||||
export function collectPackageDistImportErrors(params) {
|
||||
const files = [...new Set(params.files.map(normalizePackagePath))];
|
||||
const fileSet = new Set(files);
|
||||
const errors = [];
|
||||
|
||||
for (const importerPath of files.toSorted((left, right) => left.localeCompare(right))) {
|
||||
if (!JS_DIST_FILE_RE.test(importerPath) || importerPath.includes("/node_modules/")) {
|
||||
continue;
|
||||
}
|
||||
const source = params.readText(importerPath);
|
||||
for (const specifier of collectImportSpecifiers(source)) {
|
||||
const importedPath = resolveDistImportPath(importerPath, specifier);
|
||||
if (!importedPath || fileSet.has(importedPath)) {
|
||||
continue;
|
||||
}
|
||||
errors.push(`${importerPath} imports missing ${importedPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
@@ -226,6 +226,15 @@ restore_local_dist_from_image() {
|
||||
docker rm -f "$container_id" >/dev/null
|
||||
}
|
||||
|
||||
ensure_local_update_dist_import_closure() {
|
||||
if node scripts/check-package-dist-imports.mjs "$ROOT_DIR"; then
|
||||
return 0
|
||||
fi
|
||||
echo "WARN: reused Docker image dist failed import-closure check; rebuilding local release artifacts" >&2
|
||||
pnpm build
|
||||
pnpm ui:build
|
||||
}
|
||||
|
||||
prepare_update_tarball() {
|
||||
local pack_json
|
||||
local baseline_pack_json
|
||||
@@ -241,6 +250,7 @@ prepare_update_tarball() {
|
||||
echo "==> Build local release artifacts for update smoke"
|
||||
if [[ -n "$UPDATE_DIST_IMAGE" ]]; then
|
||||
restore_local_dist_from_image "$UPDATE_DIST_IMAGE"
|
||||
ensure_local_update_dist_import_closure
|
||||
elif [[ "$UPDATE_SKIP_LOCAL_BUILD" != "1" ]]; then
|
||||
pnpm build
|
||||
pnpm ui:build
|
||||
@@ -249,6 +259,7 @@ prepare_update_tarball() {
|
||||
node -p 'JSON.parse(require("node:fs").readFileSync("package.json", "utf8")).version'
|
||||
)"
|
||||
node --import tsx scripts/write-package-dist-inventory.ts
|
||||
node scripts/check-package-dist-imports.mjs "$ROOT_DIR"
|
||||
quiet_npm pack --ignore-scripts --json --pack-destination "$UPDATE_DIR" >"$pack_json_file"
|
||||
fi
|
||||
UPDATE_TGZ_FILE="$(
|
||||
@@ -262,6 +273,9 @@ if (!last || typeof last.filename !== "string" || last.filename.length === 0) {
|
||||
process.stdout.write(last.filename);
|
||||
' "$pack_json_file"
|
||||
)"
|
||||
if [[ -z "$UPDATE_PACKAGE_SPEC" ]]; then
|
||||
node scripts/check-openclaw-package-tarball.mjs "${UPDATE_DIR}/${UPDATE_TGZ_FILE}"
|
||||
fi
|
||||
print_pack_audit "update" "$pack_json_file"
|
||||
assert_pack_unpacked_size_budget "update" "$pack_json_file"
|
||||
packed_update_version="$(
|
||||
|
||||
@@ -354,11 +354,14 @@ describeLive("gateway live (cli backend)", () => {
|
||||
{
|
||||
sessionKey,
|
||||
idempotencyKey: `idem-${randomUUID()}`,
|
||||
message: enableCliModelSwitchProbe
|
||||
? `Please include the token CLI-BACKEND-${nonce} in your reply.` +
|
||||
` Also remember this session note for later: ${memoryToken}.` +
|
||||
" Do not include the note in your reply."
|
||||
: `Please include the token CLI-BACKEND-${nonce} in your reply.`,
|
||||
message:
|
||||
providerId === "codex-cli"
|
||||
? `Do not inspect files or run tools. Reply with exactly: CLI-BACKEND-${nonce}.`
|
||||
: enableCliModelSwitchProbe
|
||||
? `Please include the token CLI-BACKEND-${nonce} in your reply.` +
|
||||
` Also remember this session note for later: ${memoryToken}.` +
|
||||
" Do not include the note in your reply."
|
||||
: `Please include the token CLI-BACKEND-${nonce} in your reply.`,
|
||||
deliver: false,
|
||||
timeout: CLI_BACKEND_AGENT_TIMEOUT_SECONDS,
|
||||
},
|
||||
@@ -457,7 +460,7 @@ describeLive("gateway live (cli backend)", () => {
|
||||
idempotencyKey: `idem-${randomUUID()}`,
|
||||
message:
|
||||
providerId === "codex-cli"
|
||||
? `Please include the token CLI-RESUME-${resumeNonce} in your reply.`
|
||||
? `Do not inspect files or run tools. Reply with exactly: CLI-RESUME-${resumeNonce}.`
|
||||
: `Reply with exactly: CLI backend RESUME OK ${resumeNonce}.`,
|
||||
deliver: false,
|
||||
timeout: CLI_BACKEND_AGENT_TIMEOUT_SECONDS,
|
||||
|
||||
14
test/helpers/media-generation/live-video-skip-reason.test.ts
Normal file
14
test/helpers/media-generation/live-video-skip-reason.test.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveLiveVideoSkipReason } from "./live-video-skip-reason.js";
|
||||
|
||||
describe("resolveLiveVideoSkipReason", () => {
|
||||
it("classifies provider policy moderation blocks as skip-worthy drift", () => {
|
||||
expect(resolveLiveVideoSkipReason("Your request was blocked by our moderation system.")).toBe(
|
||||
"provider policy drift",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not hide ordinary provider failures", () => {
|
||||
expect(resolveLiveVideoSkipReason("video generation returned an empty asset")).toBeNull();
|
||||
});
|
||||
});
|
||||
44
test/helpers/media-generation/live-video-skip-reason.ts
Normal file
44
test/helpers/media-generation/live-video-skip-reason.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
isAuthErrorMessage,
|
||||
isBillingErrorMessage,
|
||||
isModelNotFoundErrorMessage,
|
||||
isOverloadedErrorMessage,
|
||||
isServerErrorMessage,
|
||||
isTimeoutErrorMessage,
|
||||
} from "openclaw/plugin-sdk/test-env";
|
||||
|
||||
export function resolveLiveVideoSkipReason(message: string): string | null {
|
||||
if (isAuthErrorMessage(message)) {
|
||||
return "auth drift";
|
||||
}
|
||||
if (isModelNotFoundErrorMessage(message)) {
|
||||
return "model drift";
|
||||
}
|
||||
if (isBillingErrorMessage(message)) {
|
||||
return "billing drift";
|
||||
}
|
||||
if (
|
||||
isTimeoutErrorMessage(message) ||
|
||||
/did not finish in time/i.test(message) ||
|
||||
/last status:\s*in_progress/i.test(message)
|
||||
) {
|
||||
return "provider timeout";
|
||||
}
|
||||
if (isOverloadedErrorMessage(message) || isServerErrorMessage(message)) {
|
||||
return "provider outage";
|
||||
}
|
||||
if (
|
||||
/HTTP\s+404/i.test(message) &&
|
||||
/Invalid URL/i.test(message) &&
|
||||
/\/platform\/video_gen/i.test(message)
|
||||
) {
|
||||
return "provider endpoint drift";
|
||||
}
|
||||
if (/access denied|not authorized|not enabled|permission denied/i.test(message)) {
|
||||
return "provider/model drift";
|
||||
}
|
||||
if (/blocked by (?:our )?moderation system|content policy|policy violation/i.test(message)) {
|
||||
return "provider policy drift";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -94,6 +94,39 @@ describe("check-openclaw-package-tarball", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects dist files that import missing relative chunks", () => {
|
||||
withTarball(
|
||||
["dist/cli/run-main.js"],
|
||||
{ "dist/cli/run-main.js": 'await import("../memory-state-old.js");\n' },
|
||||
(tarball) => {
|
||||
const result = spawnSync("node", [CHECK_SCRIPT, tarball], { encoding: "utf8" });
|
||||
|
||||
expect(result.status).not.toBe(0);
|
||||
expect(result.stderr).toContain(
|
||||
"dist/cli/run-main.js imports missing dist/memory-state-old.js",
|
||||
);
|
||||
},
|
||||
"2026.4.27",
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts dist files whose relative chunks are present", () => {
|
||||
withTarball(
|
||||
["dist/cli/run-main.js", "dist/memory-state-current.js"],
|
||||
{
|
||||
"dist/cli/run-main.js": 'await import("../memory-state-current.js");\n',
|
||||
"dist/memory-state-current.js": "export {};\n",
|
||||
},
|
||||
(tarball) => {
|
||||
const result = spawnSync("node", [CHECK_SCRIPT, tarball], { encoding: "utf8" });
|
||||
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
expect(result.stdout).toContain("OpenClaw package tarball integrity passed.");
|
||||
},
|
||||
"2026.4.27",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects missing Control UI assets", () => {
|
||||
withTarball(
|
||||
["dist/index.js"],
|
||||
|
||||
@@ -53,11 +53,18 @@ describe("test-install-sh-docker", () => {
|
||||
|
||||
it("can reuse dist from the already-built root Docker smoke image", () => {
|
||||
const script = readFileSync(SCRIPT_PATH, "utf8");
|
||||
const dockerfile = readFileSync("Dockerfile", "utf8");
|
||||
|
||||
expect(script).toContain('UPDATE_DIST_IMAGE="${OPENCLAW_INSTALL_SMOKE_UPDATE_DIST_IMAGE:-}"');
|
||||
expect(script).toContain("restore_local_dist_from_image");
|
||||
expect(script).toContain('docker cp "${container_id}:/app/dist" "$ROOT_DIR/dist"');
|
||||
expect(script).toContain('echo "==> Reuse local dist/ from Docker image: $image"');
|
||||
expect(script).toContain("ensure_local_update_dist_import_closure");
|
||||
expect(script).toContain('node scripts/check-package-dist-imports.mjs "$ROOT_DIR"');
|
||||
expect(script).toContain("WARN: reused Docker image dist failed import-closure check");
|
||||
expect(script).toContain("pnpm build");
|
||||
expect(script).toContain("pnpm ui:build");
|
||||
expect(dockerfile).toContain("node scripts/check-package-dist-imports.mjs /app");
|
||||
});
|
||||
|
||||
it("allows repository branch history and release tags for secret-backed Docker release checks", () => {
|
||||
@@ -92,7 +99,9 @@ describe("test-install-sh-docker", () => {
|
||||
const script = readFileSync(SCRIPT_PATH, "utf8");
|
||||
|
||||
expect(script).toContain("node --import tsx scripts/write-package-dist-inventory.ts");
|
||||
expect(script).toContain('node scripts/check-package-dist-imports.mjs "$ROOT_DIR"');
|
||||
expect(script).toContain("quiet_npm pack --ignore-scripts");
|
||||
expect(script).toContain("node scripts/check-openclaw-package-tarball.mjs");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user