mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-21 13:44:03 +00:00
fix(update): bootstrap pnpm for dev preflight
This commit is contained in:
@@ -96,7 +96,7 @@ High-level:
|
||||
3. Fetches upstream (dev only).
|
||||
4. Dev only: preflight lint + TypeScript build in a temp worktree; if the tip fails, walks back up to 10 commits to find the newest clean build.
|
||||
5. Rebases onto the selected commit (dev only).
|
||||
6. Installs deps (pnpm preferred; npm fallback; bun remains available as a secondary compatibility fallback).
|
||||
6. Installs deps with the repo package manager. For pnpm checkouts, the updater bootstraps `pnpm` on demand (via `corepack` first, then a temporary `npm install pnpm@10` fallback) instead of running `npm run build` inside a pnpm workspace.
|
||||
7. Builds + builds the Control UI.
|
||||
8. Runs `openclaw doctor` as the final “safe update” check.
|
||||
9. Syncs plugins to the active channel (dev uses bundled extensions; stable/beta uses npm) and updates npm-installed plugins.
|
||||
|
||||
@@ -26,6 +26,21 @@ function makeResult(
|
||||
}
|
||||
|
||||
describe("inferUpdateFailureHints", () => {
|
||||
it("returns a package-manager bootstrap hint for required manager failures", () => {
|
||||
const result = {
|
||||
status: "error",
|
||||
mode: "git",
|
||||
reason: "required-manager-unavailable",
|
||||
steps: [],
|
||||
durationMs: 1,
|
||||
} satisfies UpdateRunResult;
|
||||
|
||||
const hints = inferUpdateFailureHints(result);
|
||||
|
||||
expect(hints.join("\n")).toContain("requires its declared package manager");
|
||||
expect(hints.join("\n")).toContain("Install the missing package manager manually");
|
||||
});
|
||||
|
||||
it("returns EACCES hint for global update permission failures", () => {
|
||||
const result = makeResult(
|
||||
"global update",
|
||||
|
||||
@@ -37,7 +37,16 @@ function getStepLabel(step: UpdateStepInfo): string {
|
||||
}
|
||||
|
||||
export function inferUpdateFailureHints(result: UpdateRunResult): string[] {
|
||||
if (result.status !== "error" || result.mode !== "npm") {
|
||||
if (result.status !== "error") {
|
||||
return [];
|
||||
}
|
||||
if (result.reason === "required-manager-unavailable") {
|
||||
return [
|
||||
"This checkout requires its declared package manager and the updater could not bootstrap it automatically.",
|
||||
"Install the missing package manager manually, then rerun the update command.",
|
||||
];
|
||||
}
|
||||
if (result.mode !== "npm") {
|
||||
return [];
|
||||
}
|
||||
const failedStep = [...result.steps].toReversed().find((step) => step.exitCode !== 0);
|
||||
|
||||
@@ -573,6 +573,68 @@ describe("runGatewayUpdate", () => {
|
||||
expect(pnpmEnvPaths.some((value) => value.includes("openclaw-update-pnpm-"))).toBe(true);
|
||||
});
|
||||
|
||||
it("does not fall back to npm scripts when a pnpm repo cannot bootstrap pnpm", async () => {
|
||||
await setupGitPackageManagerFixture();
|
||||
const calls: string[] = [];
|
||||
const upstreamSha = "upstream123";
|
||||
|
||||
const runCommand = async (
|
||||
argv: string[],
|
||||
_options?: { env?: NodeJS.ProcessEnv; cwd?: string; timeoutMs?: number },
|
||||
) => {
|
||||
const key = argv.join(" ");
|
||||
calls.push(key);
|
||||
|
||||
if (key === `git -C ${tempDir} rev-parse --show-toplevel`) {
|
||||
return { stdout: tempDir, stderr: "", code: 0 };
|
||||
}
|
||||
if (key === `git -C ${tempDir} rev-parse HEAD`) {
|
||||
return { stdout: "abc123", stderr: "", code: 0 };
|
||||
}
|
||||
if (key === `git -C ${tempDir} rev-parse --abbrev-ref HEAD`) {
|
||||
return { stdout: "main", stderr: "", code: 0 };
|
||||
}
|
||||
if (key === `git -C ${tempDir} status --porcelain -- :!dist/control-ui/`) {
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
}
|
||||
if (key === `git -C ${tempDir} rev-parse --abbrev-ref --symbolic-full-name @{upstream}`) {
|
||||
return { stdout: "origin/main", stderr: "", code: 0 };
|
||||
}
|
||||
if (key === `git -C ${tempDir} fetch --all --prune --tags`) {
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
}
|
||||
if (key === `git -C ${tempDir} rev-parse @{upstream}`) {
|
||||
return { stdout: upstreamSha, stderr: "", code: 0 };
|
||||
}
|
||||
if (key === `git -C ${tempDir} rev-list --max-count=10 ${upstreamSha}`) {
|
||||
return { stdout: `${upstreamSha}\n`, stderr: "", code: 0 };
|
||||
}
|
||||
if (key === "pnpm --version") {
|
||||
throw new Error("spawn pnpm ENOENT");
|
||||
}
|
||||
if (key === "corepack --version") {
|
||||
throw new Error("spawn corepack ENOENT");
|
||||
}
|
||||
if (key === "npm --version") {
|
||||
return { stdout: "10.0.0", stderr: "", code: 0 };
|
||||
}
|
||||
if (key.startsWith("npm install --prefix ") && key.endsWith(" pnpm@10")) {
|
||||
return { stdout: "", stderr: "network exploded", code: 1 };
|
||||
}
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
};
|
||||
|
||||
const result = await runWithCommand(runCommand, { channel: "dev" });
|
||||
|
||||
expect(result.status).toBe("error");
|
||||
expect(result.reason).toBe("required-manager-unavailable");
|
||||
expect(calls.some((call) => call === "npm run build")).toBe(false);
|
||||
expect(calls.some((call) => call === "npm run lint")).toBe(false);
|
||||
expect(calls.some((call) => call.startsWith("git -C /tmp/openclaw-update-preflight-"))).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("skips update when no git root", async () => {
|
||||
await fs.writeFile(
|
||||
path.join(tempDir, "package.json"),
|
||||
|
||||
@@ -91,8 +91,10 @@ type BuildManager = "pnpm" | "bun" | "npm";
|
||||
type ResolvedBuildManager = {
|
||||
manager: BuildManager;
|
||||
fallback: boolean;
|
||||
preferred: BuildManager;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
cleanup?: () => Promise<void>;
|
||||
requiredPreferredMissing?: boolean;
|
||||
};
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 20 * 60_000;
|
||||
@@ -375,10 +377,13 @@ async function resolveAvailableManager(
|
||||
root: string,
|
||||
timeoutMs: number,
|
||||
baseEnv?: NodeJS.ProcessEnv,
|
||||
opts?: {
|
||||
requirePreferred?: boolean;
|
||||
},
|
||||
): Promise<ResolvedBuildManager> {
|
||||
const preferred = await detectPackageManager(root);
|
||||
if (preferred === "pnpm" && (await ensurePnpmAvailable(runCommand, timeoutMs, baseEnv))) {
|
||||
return { manager: "pnpm", fallback: false };
|
||||
return { manager: "pnpm", fallback: false, preferred };
|
||||
}
|
||||
if (preferred === "pnpm" && (await isManagerAvailable(runCommand, "npm", timeoutMs, baseEnv))) {
|
||||
const pnpmBootstrap = await bootstrapPnpmViaNpm({
|
||||
@@ -390,17 +395,26 @@ async function resolveAvailableManager(
|
||||
return {
|
||||
manager: "pnpm",
|
||||
fallback: false,
|
||||
preferred,
|
||||
env: pnpmBootstrap.env,
|
||||
cleanup: pnpmBootstrap.cleanup,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (preferred === "pnpm" && opts?.requirePreferred) {
|
||||
return {
|
||||
manager: "pnpm",
|
||||
fallback: false,
|
||||
preferred,
|
||||
requiredPreferredMissing: true,
|
||||
};
|
||||
}
|
||||
for (const manager of managerPreferenceOrder(preferred)) {
|
||||
if (await isManagerAvailable(runCommand, manager, timeoutMs, baseEnv)) {
|
||||
return { manager, fallback: manager !== preferred };
|
||||
return { manager, fallback: manager !== preferred, preferred };
|
||||
}
|
||||
}
|
||||
return { manager: "npm", fallback: preferred !== "npm" };
|
||||
return { manager: "npm", fallback: preferred !== "npm", preferred };
|
||||
}
|
||||
|
||||
type RunStepOptions = {
|
||||
@@ -720,7 +734,19 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise<
|
||||
gitRoot,
|
||||
timeoutMs,
|
||||
defaultCommandEnv,
|
||||
{ requirePreferred: true },
|
||||
);
|
||||
if (manager.requiredPreferredMissing) {
|
||||
return {
|
||||
status: "error",
|
||||
mode: "git",
|
||||
root: gitRoot,
|
||||
reason: "required-manager-unavailable",
|
||||
before: { sha: beforeSha, version: beforeVersion },
|
||||
steps,
|
||||
durationMs: Date.now() - startedAt,
|
||||
};
|
||||
}
|
||||
const preflightRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-update-preflight-"));
|
||||
const worktreeDir = path.join(preflightRoot, "worktree");
|
||||
const worktreeStep = await runStep(
|
||||
@@ -909,7 +935,19 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise<
|
||||
gitRoot,
|
||||
timeoutMs,
|
||||
defaultCommandEnv,
|
||||
{ requirePreferred: true },
|
||||
);
|
||||
if (manager.requiredPreferredMissing) {
|
||||
return {
|
||||
status: "error",
|
||||
mode: "git",
|
||||
root: gitRoot,
|
||||
reason: "required-manager-unavailable",
|
||||
before: { sha: beforeSha, version: beforeVersion },
|
||||
steps,
|
||||
durationMs: Date.now() - startedAt,
|
||||
};
|
||||
}
|
||||
try {
|
||||
const depsStep = await runStep(
|
||||
step(
|
||||
|
||||
Reference in New Issue
Block a user