mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
* fix(update): preserve installs on unsupported Node * fix(update): verify skipped install scripts * refactor(update): share global install preflight * fix(update): validate Bun-installed Node runtime * fix(update): classify install preflight failures * refactor(update): normalize install failure reasons * style(update): format updater policy test * fix(update): preserve pnpm script policy safety * fix(update): classify hosted git sources * chore: leave release notes to release flow * fix(release): defer package inventory helper loading * style(release): keep inventory loader LOC-neutral * fix(update): validate staged package lifecycle * fix(update): match packed lifecycle contract * fix(update): harden Bun package activation * test(update): model packed Bun lifecycle * style(update): avoid lifecycle name shadowing * fix(update): remove unsafe Bun staging experiment * fix(update): satisfy updater type checks * fix(update): preserve packed npm package name * fix(update): preserve checkout failure status * fix(update): disable scripts while packing candidates * fix(update): preflight source package engines safely * fix(update): preserve legacy npm downgrades * fix(update): pin npm packing to selected Node * fix(update): stage npm activation before replacing live package * fix(update): guard non-npm source activation * refactor(update): isolate npm staging helpers * fix(update): harden staged package lifecycle * test(update): satisfy merged type gates * fix(update): privatize runtime npm helper * fix(ci): satisfy merged lint and type gates * docs(changelog): defer updater note to release * fix(update): guard package activation runtime * fix(update): preserve installs on unsupported Node * test(update): reject prerelease Node runtimes * test(update): use shared temp cleanup * fix(update): fail closed when install scripts are skipped * fix(update): pack install guard in docker artifacts * fix(ci): keep install guard export tooling-local
156 lines
5.4 KiB
TypeScript
156 lines
5.4 KiB
TypeScript
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { sortUniqueStrings } from "@openclaw/normalization-core/string-normalization";
|
|
import { writeJson } from "../../src/infra/json-files.ts";
|
|
import {
|
|
collectPackageDistInventory,
|
|
PACKAGE_DIST_INVENTORY_RELATIVE_PATH,
|
|
} from "../../src/infra/package-dist-inventory.ts";
|
|
|
|
export { LOCAL_BUILD_METADATA_DIST_PATHS } from "./local-build-metadata-paths.mjs";
|
|
export { PACKAGE_DIST_INVENTORY_RELATIVE_PATH };
|
|
|
|
export const PACKAGE_INSTALL_GUARD_RELATIVE_PATH = "dist/openclaw-install-guard";
|
|
|
|
const INSTALL_STAGE_DEBRIS_DIR_PATTERN = /^\.openclaw-install-stage(?:-[^/]+)?$/iu;
|
|
|
|
function normalizeRelativePath(value: string): string {
|
|
return value.replace(/\\/g, "/");
|
|
}
|
|
|
|
function isInstallStageDirName(value: string): boolean {
|
|
return INSTALL_STAGE_DEBRIS_DIR_PATTERN.test(value);
|
|
}
|
|
|
|
export function isLegacyPluginDependencyInstallStagePath(relativePath: string): boolean {
|
|
const parts = normalizeRelativePath(relativePath).split("/");
|
|
return (
|
|
parts.length >= 4 &&
|
|
parts[0]?.toLowerCase() === "dist" &&
|
|
parts[1]?.toLowerCase() === "extensions" &&
|
|
Boolean(parts[2]) &&
|
|
isInstallStageDirName(parts[3] ?? "")
|
|
);
|
|
}
|
|
|
|
async function collectLegacyPluginDependencyStagingDebrisPaths(
|
|
packageRoot: string,
|
|
): Promise<string[]> {
|
|
const distDirs: string[] = [];
|
|
try {
|
|
const packageRootEntries = await fs.readdir(packageRoot, { withFileTypes: true });
|
|
for (const entry of packageRootEntries) {
|
|
if (entry.isDirectory() && entry.name.toLowerCase() === "dist") {
|
|
distDirs.push(path.join(packageRoot, entry.name));
|
|
}
|
|
}
|
|
} catch (error) {
|
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
return [];
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
const debris: string[] = [];
|
|
for (const distDir of distDirs) {
|
|
let distEntries: import("node:fs").Dirent[];
|
|
try {
|
|
distEntries = await fs.readdir(distDir, { withFileTypes: true });
|
|
} catch (error) {
|
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
continue;
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
for (const distEntry of distEntries) {
|
|
if (!distEntry.isDirectory() || distEntry.name.toLowerCase() !== "extensions") {
|
|
continue;
|
|
}
|
|
const extensionsDir = path.join(distDir, distEntry.name);
|
|
let extensionEntries: import("node:fs").Dirent[];
|
|
try {
|
|
extensionEntries = await fs.readdir(extensionsDir, { withFileTypes: true });
|
|
} catch (error) {
|
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
continue;
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
for (const extensionEntry of extensionEntries) {
|
|
if (!extensionEntry.isDirectory()) {
|
|
continue;
|
|
}
|
|
const extensionPath = path.join(extensionsDir, extensionEntry.name);
|
|
let stagingEntries: import("node:fs").Dirent[];
|
|
try {
|
|
stagingEntries = await fs.readdir(extensionPath, { withFileTypes: true });
|
|
} catch (error) {
|
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
continue;
|
|
}
|
|
throw error;
|
|
}
|
|
for (const stagingEntry of stagingEntries) {
|
|
if (!isInstallStageDirName(stagingEntry.name)) {
|
|
continue;
|
|
}
|
|
debris.push(
|
|
normalizeRelativePath(
|
|
path.relative(packageRoot, path.join(extensionPath, stagingEntry.name)),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return debris.toSorted((left, right) => left.localeCompare(right));
|
|
}
|
|
|
|
async function assertNoLegacyPluginDependencyStagingDebris(packageRoot: string): Promise<void> {
|
|
const debris = await collectLegacyPluginDependencyStagingDebrisPaths(packageRoot);
|
|
if (debris.length === 0) {
|
|
return;
|
|
}
|
|
throw new Error(
|
|
`unexpected legacy plugin dependency staging debris in package dist: ${debris.join(", ")}`,
|
|
);
|
|
}
|
|
|
|
async function writePackageDistInventoryFile(
|
|
packageRoot: string,
|
|
entries: string[],
|
|
): Promise<string[]> {
|
|
// The packed guard intentionally stays outside the inventory until preinstall removes it.
|
|
// An updater that skips lifecycle scripts rejects the staged package before activation.
|
|
const inventory = sortUniqueStrings(
|
|
entries.filter((relativePath) => relativePath !== PACKAGE_INSTALL_GUARD_RELATIVE_PATH),
|
|
);
|
|
const inventoryPath = path.join(packageRoot, PACKAGE_DIST_INVENTORY_RELATIVE_PATH);
|
|
await writeJson(inventoryPath, inventory, { trailingNewline: true });
|
|
return inventory;
|
|
}
|
|
|
|
export async function writePackageDistInventory(packageRoot: string): Promise<string[]> {
|
|
await assertNoLegacyPluginDependencyStagingDebris(packageRoot);
|
|
return writePackageDistInventoryFile(packageRoot, await collectPackageDistInventory(packageRoot));
|
|
}
|
|
|
|
async function writePackageInstallGuardMarker(packageRoot: string): Promise<void> {
|
|
const markerPath = path.join(packageRoot, PACKAGE_INSTALL_GUARD_RELATIVE_PATH);
|
|
await fs.mkdir(path.dirname(markerPath), { recursive: true });
|
|
await fs.writeFile(markerPath, "OpenClaw package preinstall has not completed.\n", "utf8");
|
|
}
|
|
|
|
export async function writePackageDistInventoryForPublish(
|
|
packageRoot: string,
|
|
inventory?: string[],
|
|
): Promise<string[]> {
|
|
await assertNoLegacyPluginDependencyStagingDebris(packageRoot);
|
|
const entries = inventory ?? (await collectPackageDistInventory(packageRoot));
|
|
const writtenInventory = await writePackageDistInventoryFile(packageRoot, entries);
|
|
await writePackageInstallGuardMarker(packageRoot);
|
|
return writtenInventory;
|
|
}
|