Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
361 lines
9.9 KiB
TypeScript
361 lines
9.9 KiB
TypeScript
// Smoke Common helper supports OpenClaw script workflows.
|
|
import { readFile, rm } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { extractLastOpenClawVersionFromLog } from "./filesystem.ts";
|
|
import { run, say } from "./host-command.ts";
|
|
import { resolveHostIp, resolveHostPort } from "./host-server.ts";
|
|
import { startHostServer } from "./host-server.ts";
|
|
import { runSmokeLane, type SmokeLane, type SmokeLaneStatus } from "./lane-runner.ts";
|
|
import {
|
|
packageBuildCommitFromTgz,
|
|
packageVersionFromTgz,
|
|
packOpenClaw,
|
|
} from "./package-artifact.ts";
|
|
import type { HostServer, Mode, PackageArtifact, Provider, SnapshotInfo } from "./types.ts";
|
|
|
|
export interface SmokeHostOptions {
|
|
hostIp?: string;
|
|
hostPort: number;
|
|
hostPortExplicit: boolean;
|
|
}
|
|
|
|
export interface SmokeRunOptions {
|
|
installVersion?: string;
|
|
json: boolean;
|
|
keepServer: boolean;
|
|
mode: Mode;
|
|
provider: Provider;
|
|
snapshotHint: string;
|
|
targetPackageSpec?: string;
|
|
}
|
|
|
|
export interface SmokeLaneStatuses {
|
|
freshAgent: string;
|
|
freshGateway: string;
|
|
freshMain: string;
|
|
freshVersion: string;
|
|
latestInstalledVersion: string;
|
|
upgrade: string;
|
|
upgradeAgent: string;
|
|
upgradeGateway: string;
|
|
upgradeVersion: string;
|
|
}
|
|
|
|
export interface CommonSmokeSummary {
|
|
currentHead: string;
|
|
freshMain: {
|
|
agent: string;
|
|
gateway: string;
|
|
status: string;
|
|
version: string;
|
|
};
|
|
installVersion: string;
|
|
latestVersion: string;
|
|
mode: Mode;
|
|
provider: Provider;
|
|
runDir: string;
|
|
snapshotHint: string;
|
|
snapshotId: string;
|
|
targetPackageSpec: string;
|
|
upgrade: {
|
|
agent: string;
|
|
gateway: string;
|
|
latestVersionInstalled: string;
|
|
mainVersion: string;
|
|
status: string;
|
|
};
|
|
vm: string;
|
|
}
|
|
|
|
export abstract class SmokeRunController<TOptions extends SmokeRunOptions & SmokeHostOptions> {
|
|
protected hostIp = "";
|
|
protected hostPort = 0;
|
|
protected runDir = "";
|
|
protected server: HostServer | null = null;
|
|
protected tgzDir = "";
|
|
|
|
protected constructor(protected options: TOptions) {}
|
|
|
|
protected abstract runFreshLane(): Promise<void>;
|
|
protected abstract runUpgradeLane(): Promise<void>;
|
|
protected abstract writeSummary(): Promise<string>;
|
|
protected abstract printSummary(summaryPath: string): void;
|
|
protected abstract status: Pick<SmokeLaneStatuses, "freshMain" | "upgrade">;
|
|
|
|
protected async prepareHost(
|
|
defaultPort: number,
|
|
latestVersion: string,
|
|
snapshot: SnapshotInfo,
|
|
vmName: string,
|
|
): Promise<void> {
|
|
[this.hostIp, this.hostPort] = await prepareSmokeRunHost(
|
|
this.options,
|
|
defaultPort,
|
|
latestVersion,
|
|
this.runDir,
|
|
snapshot,
|
|
this.options.snapshotHint,
|
|
vmName,
|
|
);
|
|
}
|
|
|
|
protected async runLanesAndFinish(): Promise<void> {
|
|
await runSmokeLanesAndFinish(
|
|
this.options.mode,
|
|
this.options.json,
|
|
this.status,
|
|
async () => this.runFreshLane(),
|
|
async () => this.runUpgradeLane(),
|
|
async () => this.writeSummary(),
|
|
(pathLocal) => this.printSummary(pathLocal),
|
|
);
|
|
}
|
|
|
|
protected async cleanupArtifacts(): Promise<void> {
|
|
await cleanupSmokeArtifacts({
|
|
keepServer: this.options.keepServer,
|
|
server: this.server,
|
|
tgzDir: this.tgzDir,
|
|
});
|
|
}
|
|
}
|
|
|
|
export async function resolveSmokeHostConfig(
|
|
options: SmokeHostOptions,
|
|
defaultPort: number,
|
|
): Promise<{ hostIp: string; hostPort: number }> {
|
|
return {
|
|
hostIp: resolveHostIp(options.hostIp),
|
|
hostPort: await resolveHostPort(options.hostPort, options.hostPortExplicit, defaultPort),
|
|
};
|
|
}
|
|
|
|
export async function prepareSmokeRunHost(
|
|
options: SmokeHostOptions,
|
|
defaultPort: number,
|
|
latestVersion: string,
|
|
runDir: string,
|
|
snapshot: SnapshotInfo,
|
|
snapshotHint: string,
|
|
vmName: string,
|
|
): Promise<readonly [hostIp: string, hostPort: number]> {
|
|
const host = await resolveSmokeHostConfig(options, defaultPort);
|
|
logSmokeRunStart({
|
|
latestVersion,
|
|
runDir,
|
|
snapshot,
|
|
snapshotHint,
|
|
vmName,
|
|
});
|
|
return [host.hostIp, host.hostPort];
|
|
}
|
|
|
|
export function logSmokeRunStart(input: {
|
|
latestVersion: string;
|
|
runDir: string;
|
|
snapshot: SnapshotInfo;
|
|
snapshotHint: string;
|
|
vmName: string;
|
|
}): void {
|
|
say(`VM: ${input.vmName}`);
|
|
say(`Snapshot hint: ${input.snapshotHint}`);
|
|
say(`Resolved snapshot: ${input.snapshot.name} [${input.snapshot.state}]`);
|
|
say(`Latest npm version: ${input.latestVersion}`);
|
|
say(`Current head: ${currentGitHeadShort()}`);
|
|
say(`Run logs: ${input.runDir}`);
|
|
}
|
|
|
|
export async function startSmokeArtifactServer(input: {
|
|
artifact: PackageArtifact;
|
|
dir: string;
|
|
hostIp: string;
|
|
label: string;
|
|
port: number;
|
|
}): Promise<{ hostPort: number; server: HostServer }> {
|
|
const server = await startHostServer({
|
|
artifactPath: input.artifact.path,
|
|
dir: input.dir,
|
|
hostIp: input.hostIp,
|
|
label: input.label,
|
|
port: input.port,
|
|
});
|
|
return { hostPort: server.port, server };
|
|
}
|
|
|
|
export async function packAndServeSmokeArtifact(
|
|
tgzDir: string,
|
|
packageSpec: string | undefined,
|
|
hostIp: string,
|
|
hostPort: number,
|
|
label: string,
|
|
requireControlUi = false,
|
|
): Promise<readonly [artifact: PackageArtifact, server: HostServer, hostPort: number]> {
|
|
const artifact = await packOpenClaw({
|
|
destination: tgzDir,
|
|
packageSpec,
|
|
requireControlUi,
|
|
});
|
|
const server = await startSmokeArtifactServer({
|
|
artifact,
|
|
dir: tgzDir,
|
|
hostIp,
|
|
label,
|
|
port: hostPort,
|
|
});
|
|
return [artifact, server.server, server.hostPort];
|
|
}
|
|
|
|
export async function runRequestedSmokeLanes(input: {
|
|
mode: Mode;
|
|
runFresh: () => Promise<void>;
|
|
runLane: (name: "fresh" | "upgrade", fn: () => Promise<void>) => Promise<void>;
|
|
runUpgrade: () => Promise<void>;
|
|
}): Promise<void> {
|
|
if (input.mode === "fresh" || input.mode === "both") {
|
|
await input.runLane("fresh", input.runFresh);
|
|
}
|
|
if (input.mode === "upgrade" || input.mode === "both") {
|
|
await input.runLane("upgrade", input.runUpgrade);
|
|
}
|
|
}
|
|
|
|
export async function runSmokeLaneWithStatus(
|
|
name: "fresh" | "upgrade",
|
|
fn: () => Promise<void>,
|
|
statuses: Pick<SmokeLaneStatuses, "freshMain" | "upgrade">,
|
|
): Promise<void> {
|
|
await runSmokeLane(name, fn, (lane, status) => setSmokeLaneStatus(statuses, lane, status));
|
|
}
|
|
|
|
export function setSmokeLaneStatus(
|
|
statuses: Pick<SmokeLaneStatuses, "freshMain" | "upgrade">,
|
|
name: SmokeLane,
|
|
status: SmokeLaneStatus,
|
|
): void {
|
|
if (name === "fresh") {
|
|
statuses.freshMain = status;
|
|
} else {
|
|
statuses.upgrade = status;
|
|
}
|
|
}
|
|
|
|
export async function finishSmokeRun(input: {
|
|
json: boolean;
|
|
printSummary: (summaryPath: string) => void;
|
|
status: Pick<SmokeLaneStatuses, "freshMain" | "upgrade">;
|
|
summaryPath: string;
|
|
}): Promise<void> {
|
|
if (input.json) {
|
|
process.stdout.write(await readFile(input.summaryPath, "utf8"));
|
|
} else {
|
|
input.printSummary(input.summaryPath);
|
|
}
|
|
if (input.status.freshMain === "fail" || input.status.upgrade === "fail") {
|
|
process.exitCode = 1;
|
|
}
|
|
}
|
|
|
|
export async function runSmokeLanesAndFinish(
|
|
mode: Mode,
|
|
json: boolean,
|
|
status: Pick<SmokeLaneStatuses, "freshMain" | "upgrade">,
|
|
runFresh: () => Promise<void>,
|
|
runUpgrade: () => Promise<void>,
|
|
writeSummary: () => Promise<string>,
|
|
printSummary: (summaryPath: string) => void,
|
|
): Promise<void> {
|
|
await runRequestedSmokeLanes({
|
|
mode,
|
|
runFresh,
|
|
runLane: async (name, fn) => runSmokeLaneWithStatus(name, fn, status),
|
|
runUpgrade,
|
|
});
|
|
await finishSmokeRun({
|
|
json,
|
|
printSummary,
|
|
status,
|
|
summaryPath: await writeSummary(),
|
|
});
|
|
}
|
|
|
|
export async function cleanupSmokeArtifacts(input: {
|
|
keepServer: boolean;
|
|
server: HostServer | null;
|
|
tgzDir: string;
|
|
}): Promise<void> {
|
|
if (input.keepServer) {
|
|
return;
|
|
}
|
|
await input.server?.stop().catch(() => undefined);
|
|
await rm(input.tgzDir, { force: true, recursive: true }).catch(() => undefined);
|
|
}
|
|
|
|
export async function expectedPackageTargetVersion(artifact: PackageArtifact): Promise<string> {
|
|
return artifact.version || (await packageVersionFromTgz(artifact.path));
|
|
}
|
|
|
|
export async function expectedPackageBuildCommit(artifact: PackageArtifact): Promise<string> {
|
|
return artifact.buildCommitShort || (await packageBuildCommitFromTgz(artifact.path)).slice(0, 7);
|
|
}
|
|
|
|
export async function extractLastOpenClawVersion(
|
|
runDir: string,
|
|
phaseName: string,
|
|
pattern: RegExp,
|
|
): Promise<string> {
|
|
return await extractLastOpenClawVersionFromLog(path.join(runDir, `${phaseName}.log`), pattern);
|
|
}
|
|
|
|
export function buildCommonSmokeSummary(input: {
|
|
artifact: PackageArtifact | null;
|
|
latestVersion: string;
|
|
options: SmokeRunOptions;
|
|
runDir: string;
|
|
snapshot: SnapshotInfo;
|
|
status: SmokeLaneStatuses;
|
|
vmName: string;
|
|
}): CommonSmokeSummary {
|
|
return {
|
|
currentHead: input.artifact?.buildCommitShort || currentGitHeadShort(),
|
|
freshMain: {
|
|
agent: input.status.freshAgent,
|
|
gateway: input.status.freshGateway,
|
|
status: input.status.freshMain,
|
|
version: input.status.freshVersion,
|
|
},
|
|
installVersion: input.options.installVersion || "",
|
|
latestVersion: input.latestVersion,
|
|
mode: input.options.mode,
|
|
provider: input.options.provider,
|
|
runDir: input.runDir,
|
|
snapshotHint: input.options.snapshotHint,
|
|
snapshotId: input.snapshot.id,
|
|
targetPackageSpec: input.options.targetPackageSpec || "",
|
|
upgrade: {
|
|
agent: input.status.upgradeAgent,
|
|
gateway: input.status.upgradeGateway,
|
|
latestVersionInstalled: input.status.latestInstalledVersion,
|
|
mainVersion: input.status.upgradeVersion,
|
|
status: input.status.upgrade,
|
|
},
|
|
vm: input.vmName,
|
|
};
|
|
}
|
|
|
|
export function printSmokeTargetSummary(input: {
|
|
includeInstallVersion?: boolean;
|
|
installVersion?: string;
|
|
targetPackageSpec?: string;
|
|
}): void {
|
|
if (input.targetPackageSpec) {
|
|
process.stdout.write(` target-package: ${input.targetPackageSpec}\n`);
|
|
}
|
|
if (input.includeInstallVersion !== false && input.installVersion) {
|
|
process.stdout.write(` baseline-install-version: ${input.installVersion}\n`);
|
|
}
|
|
}
|
|
|
|
function currentGitHeadShort(): string {
|
|
return run("git", ["rev-parse", "--short", "HEAD"], { quiet: true }).stdout.trim();
|
|
}
|