fix(ci): harden full release live checks

This commit is contained in:
Peter Steinberger
2026-04-29 00:36:41 +01:00
parent 43fa40a35d
commit b04c9380ed
11 changed files with 321 additions and 47 deletions

View File

@@ -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")}`);
}

View 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.");

View 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;
}

View File

@@ -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="$(