mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:20:43 +00:00
ci(release): speed up focused release reruns
This commit is contained in:
@@ -33,6 +33,7 @@ const SUPPORTED_SUITES = new Set([
|
||||
"packaged-upgrade",
|
||||
"dev-update",
|
||||
]);
|
||||
const SUPPORTED_OS_IDS = new Set(["ubuntu", "windows", "macos"]);
|
||||
|
||||
export const CROSS_OS_AGENT_TURN_TIMEOUT_SECONDS = parsePositiveIntegerEnv(
|
||||
"OPENCLAW_CROSS_OS_AGENT_TURN_TIMEOUT_SECONDS",
|
||||
@@ -122,6 +123,10 @@ export const CROSS_OS_RELEASE_SMOKE_TOOLS_PROFILE = "minimal";
|
||||
export const CROSS_OS_WINDOWS_PACKAGED_UPGRADE_STEP_TIMEOUT_SECONDS = 25 * 60;
|
||||
export const CROSS_OS_WINDOWS_PACKAGED_UPGRADE_WRAPPER_TIMEOUT_MS =
|
||||
(CROSS_OS_WINDOWS_PACKAGED_UPGRADE_STEP_TIMEOUT_SECONDS + 5 * 60) * 1000;
|
||||
export const CROSS_OS_COMMAND_HEARTBEAT_SECONDS = parsePositiveIntegerEnv(
|
||||
"OPENCLAW_CROSS_OS_COMMAND_HEARTBEAT_SECONDS",
|
||||
60,
|
||||
);
|
||||
|
||||
if (isMainModule()) {
|
||||
try {
|
||||
@@ -232,6 +237,7 @@ export function resolveRunnerMatrix(params) {
|
||||
const pick = (...values) =>
|
||||
values.find((value) => typeof value === "string" && value.trim().length > 0)?.trim();
|
||||
const suites = resolveRequestedSuites(params.mode, params.ref);
|
||||
const suiteFilter = parseCrossOsSuiteFilter(params.suiteFilter ?? "");
|
||||
const runners = [
|
||||
{
|
||||
os_id: "ubuntu",
|
||||
@@ -252,19 +258,84 @@ export function resolveRunnerMatrix(params) {
|
||||
artifact_name: "macos",
|
||||
},
|
||||
];
|
||||
return {
|
||||
include: runners.flatMap((runner) =>
|
||||
suites.map((suite) =>
|
||||
const include = runners.flatMap((runner) =>
|
||||
suites
|
||||
.filter((suite) => suiteFilter.matches(runner.os_id, suite))
|
||||
.map((suite) =>
|
||||
Object.assign({}, runner, {
|
||||
suite,
|
||||
suite_label: formatSuiteLabel(suite),
|
||||
lane: suite.includes(`upgrade`) || suite === `dev-update` ? `upgrade` : `fresh`,
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (include.length === 0) {
|
||||
throw new Error(
|
||||
`cross_os_suite_filter ${JSON.stringify(params.suiteFilter ?? "")} did not match any ${params.mode} suite.`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
include,
|
||||
};
|
||||
}
|
||||
|
||||
export function parseCrossOsSuiteFilter(rawFilter) {
|
||||
const tokens = String(rawFilter ?? "")
|
||||
.split(/[, ]+/u)
|
||||
.map((token) => normalizeCrossOsSuiteFilterToken(token))
|
||||
.filter(Boolean);
|
||||
if (tokens.length === 0) {
|
||||
return {
|
||||
matches: () => true,
|
||||
tokens,
|
||||
};
|
||||
}
|
||||
|
||||
const matchers = tokens.map((token) => {
|
||||
if (SUPPORTED_SUITES.has(token)) {
|
||||
return { osId: "", suite: token };
|
||||
}
|
||||
if (SUPPORTED_OS_IDS.has(token)) {
|
||||
return { osId: token, suite: "" };
|
||||
}
|
||||
for (const separator of ["/", ":", "-"]) {
|
||||
const matchedOs = [...SUPPORTED_OS_IDS].find((osId) =>
|
||||
token.startsWith(`${osId}${separator}`),
|
||||
);
|
||||
if (!matchedOs) {
|
||||
continue;
|
||||
}
|
||||
const suite = token.slice(matchedOs.length + separator.length);
|
||||
if (!SUPPORTED_SUITES.has(suite)) {
|
||||
break;
|
||||
}
|
||||
return { osId: matchedOs, suite };
|
||||
}
|
||||
throw new Error(
|
||||
`Unsupported cross_os_suite_filter token ${JSON.stringify(token)}. Use an OS id, suite id, or os/suite pair such as windows/packaged-upgrade.`,
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
matches: (osId, suite) =>
|
||||
matchers.some((matcher) => {
|
||||
const osMatches = !matcher.osId || matcher.osId === osId;
|
||||
const suiteMatches = !matcher.suite || matcher.suite === suite;
|
||||
return osMatches && suiteMatches;
|
||||
}),
|
||||
tokens,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeCrossOsSuiteFilterToken(token) {
|
||||
return token
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/_/gu, "-")
|
||||
.replace(/\s*[/:-]\s*/gu, (separator) => separator.trim())
|
||||
.replace(/\s+/gu, "-");
|
||||
}
|
||||
|
||||
export function readRunnerOverrideEnv(env = process.env) {
|
||||
const preferNonEmptyEnv = (primary: string | undefined, legacy: string | undefined) => {
|
||||
const primaryValue = primary?.trim();
|
||||
@@ -319,6 +390,7 @@ async function main(argv) {
|
||||
ubuntuRunner: args["ubuntu-runner"],
|
||||
windowsRunner: args["windows-runner"],
|
||||
macosRunner: args["macos-runner"],
|
||||
suiteFilter: args["suite-filter"],
|
||||
...runnerOverrideEnv,
|
||||
}),
|
||||
)}\n`,
|
||||
@@ -733,76 +805,80 @@ async function runUpgradeLane(params) {
|
||||
const cleanup = [];
|
||||
try {
|
||||
const env = buildLaneEnv(lane, params.providerConfig, params.providerSecretValue);
|
||||
logLanePhase(lane, "install-baseline");
|
||||
if (!params.baselineTgz && params.baselineSpec) {
|
||||
await installPackageSpec({
|
||||
await runTimedLanePhase(lane, "install-baseline", async () => {
|
||||
if (!params.baselineTgz && params.baselineSpec) {
|
||||
await installPackageSpec({
|
||||
lane,
|
||||
env,
|
||||
packageSpec: params.baselineSpec,
|
||||
logPath: join(params.logsDir, "upgrade-install-baseline.log"),
|
||||
ignoreScripts: true,
|
||||
});
|
||||
} else {
|
||||
await installTarballPackage({
|
||||
lane,
|
||||
env,
|
||||
tgzPath: params.baselineTgz,
|
||||
logPath: join(params.logsDir, "upgrade-install-baseline.log"),
|
||||
ignoreScripts: true,
|
||||
restoreBundledPluginPostinstall: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
await runTimedLanePhase(lane, "run-baseline-bundled-plugin-postinstall", async () => {
|
||||
await runBundledPluginPostinstall({
|
||||
lane,
|
||||
env,
|
||||
packageSpec: params.baselineSpec,
|
||||
logPath: join(params.logsDir, "upgrade-install-baseline.log"),
|
||||
ignoreScripts: true,
|
||||
});
|
||||
} else {
|
||||
await installTarballPackage({
|
||||
lane,
|
||||
env,
|
||||
tgzPath: params.baselineTgz,
|
||||
logPath: join(params.logsDir, "upgrade-install-baseline.log"),
|
||||
ignoreScripts: true,
|
||||
restoreBundledPluginPostinstall: false,
|
||||
});
|
||||
}
|
||||
logLanePhase(lane, "run-baseline-bundled-plugin-postinstall");
|
||||
await runBundledPluginPostinstall({
|
||||
lane,
|
||||
env,
|
||||
logPath: join(params.logsDir, "upgrade-install-baseline.log"),
|
||||
});
|
||||
|
||||
const baseline = {
|
||||
version: readInstalledVersion(lane.prefixDir),
|
||||
};
|
||||
|
||||
logLanePhase(lane, "update");
|
||||
const updateEnv = buildRealUpdateEnv(env);
|
||||
const updateArgs = buildPackagedUpgradeUpdateArgs(params.candidateUrl);
|
||||
const updateLogPath = join(params.logsDir, "upgrade-update.log");
|
||||
let updateResult;
|
||||
let usedWindowsPackagedUpgradeTimeoutFallback = false;
|
||||
try {
|
||||
updateResult = await runOpenClaw({
|
||||
lane,
|
||||
env: updateEnv,
|
||||
args: updateArgs,
|
||||
logPath: updateLogPath,
|
||||
timeoutMs: updateTimeoutMs(),
|
||||
check: false,
|
||||
});
|
||||
} catch (error) {
|
||||
if (!isRecoverableWindowsPackagedUpgradeTimeoutError(error, process.platform)) {
|
||||
throw error;
|
||||
await runTimedLanePhase(lane, "update", async () => {
|
||||
try {
|
||||
updateResult = await runOpenClaw({
|
||||
lane,
|
||||
env: updateEnv,
|
||||
args: updateArgs,
|
||||
logPath: updateLogPath,
|
||||
timeoutMs: updateTimeoutMs(),
|
||||
check: false,
|
||||
});
|
||||
} catch (error) {
|
||||
if (!isRecoverableWindowsPackagedUpgradeTimeoutError(error, process.platform)) {
|
||||
throw error;
|
||||
}
|
||||
usedWindowsPackagedUpgradeTimeoutFallback = true;
|
||||
appendFileSync(
|
||||
updateLogPath,
|
||||
`\n[release-checks] Windows baseline updater timed out after fetching candidate; falling back to direct candidate install: ${formatError(error)}\n`,
|
||||
);
|
||||
updateResult = {
|
||||
exitCode: 124,
|
||||
stdout: "",
|
||||
stderr: formatError(error),
|
||||
};
|
||||
}
|
||||
usedWindowsPackagedUpgradeTimeoutFallback = true;
|
||||
appendFileSync(
|
||||
updateLogPath,
|
||||
`\n[release-checks] Windows baseline updater timed out after fetching candidate; falling back to direct candidate install: ${formatError(error)}\n`,
|
||||
);
|
||||
updateResult = {
|
||||
exitCode: 124,
|
||||
stdout: "",
|
||||
stderr: formatError(error),
|
||||
};
|
||||
}
|
||||
});
|
||||
const usedWindowsPackagedUpgradeFallback =
|
||||
usedWindowsPackagedUpgradeTimeoutFallback ||
|
||||
isRecoverableWindowsPackagedUpgradeSwapCleanupFailure(updateResult, process.platform);
|
||||
if (usedWindowsPackagedUpgradeFallback) {
|
||||
logLanePhase(lane, "update-fallback-install");
|
||||
await installPackageSpec({
|
||||
lane,
|
||||
env,
|
||||
packageSpec: params.candidateUrl,
|
||||
logPath: join(params.logsDir, "upgrade-update-fallback-install.log"),
|
||||
await runTimedLanePhase(lane, "update-fallback-install", async () => {
|
||||
await installPackageSpec({
|
||||
lane,
|
||||
env,
|
||||
packageSpec: params.candidateUrl,
|
||||
logPath: join(params.logsDir, "upgrade-update-fallback-install.log"),
|
||||
});
|
||||
});
|
||||
} else {
|
||||
verifyPackagedUpgradeUpdateResult(updateResult, {
|
||||
@@ -816,69 +892,77 @@ async function runUpgradeLane(params) {
|
||||
usedWindowsPackagedUpgradeFallback,
|
||||
})
|
||||
) {
|
||||
logLanePhase(lane, "update-status");
|
||||
await runOpenClaw({
|
||||
lane,
|
||||
env: updateEnv,
|
||||
args: ["update", "status", "--json"],
|
||||
logPath: join(params.logsDir, "upgrade-update-status.log"),
|
||||
timeoutMs: 2 * 60 * 1000,
|
||||
await runTimedLanePhase(lane, "update-status", async () => {
|
||||
await runOpenClaw({
|
||||
lane,
|
||||
env: updateEnv,
|
||||
args: ["update", "status", "--json"],
|
||||
logPath: join(params.logsDir, "upgrade-update-status.log"),
|
||||
timeoutMs: 2 * 60 * 1000,
|
||||
});
|
||||
});
|
||||
}
|
||||
logLanePhase(lane, "run-bundled-plugin-postinstall");
|
||||
await runBundledPluginPostinstall({
|
||||
lane,
|
||||
env,
|
||||
logPath: join(params.logsDir, "upgrade-bundled-plugin-postinstall.log"),
|
||||
await runTimedLanePhase(lane, "run-bundled-plugin-postinstall", async () => {
|
||||
await runBundledPluginPostinstall({
|
||||
lane,
|
||||
env,
|
||||
logPath: join(params.logsDir, "upgrade-bundled-plugin-postinstall.log"),
|
||||
});
|
||||
});
|
||||
|
||||
const installed = readInstalledMetadata(lane.prefixDir);
|
||||
verifyInstalledCandidate(installed, params.build);
|
||||
|
||||
logLanePhase(lane, "onboard");
|
||||
await runOnboard({
|
||||
lane,
|
||||
env,
|
||||
providerConfig: params.providerConfig,
|
||||
logPath: join(params.logsDir, "upgrade-onboard.log"),
|
||||
await runTimedLanePhase(lane, "onboard", async () => {
|
||||
await runOnboard({
|
||||
lane,
|
||||
env,
|
||||
providerConfig: params.providerConfig,
|
||||
logPath: join(params.logsDir, "upgrade-onboard.log"),
|
||||
});
|
||||
});
|
||||
|
||||
logLanePhase(lane, "models-set");
|
||||
await runModelsSet({
|
||||
lane,
|
||||
env,
|
||||
providerConfig: params.providerConfig,
|
||||
logPath: join(params.logsDir, "upgrade-models-set.log"),
|
||||
await runTimedLanePhase(lane, "models-set", async () => {
|
||||
await runModelsSet({
|
||||
lane,
|
||||
env,
|
||||
providerConfig: params.providerConfig,
|
||||
logPath: join(params.logsDir, "upgrade-models-set.log"),
|
||||
});
|
||||
});
|
||||
|
||||
logLanePhase(lane, "start-gateway");
|
||||
const gateway = await startGateway({
|
||||
lane,
|
||||
env,
|
||||
logPath: join(params.logsDir, "upgrade-gateway.log"),
|
||||
});
|
||||
const gateway = await runTimedLanePhase(lane, "start-gateway", async () =>
|
||||
startGateway({
|
||||
lane,
|
||||
env,
|
||||
logPath: join(params.logsDir, "upgrade-gateway.log"),
|
||||
}),
|
||||
);
|
||||
cleanup.push(() => stopGateway(gateway));
|
||||
|
||||
logLanePhase(lane, "wait-gateway");
|
||||
await waitForGateway({
|
||||
lane,
|
||||
env,
|
||||
logPath: join(params.logsDir, "upgrade-gateway-status.log"),
|
||||
await runTimedLanePhase(lane, "wait-gateway", async () => {
|
||||
await waitForGateway({
|
||||
lane,
|
||||
env,
|
||||
logPath: join(params.logsDir, "upgrade-gateway-status.log"),
|
||||
});
|
||||
});
|
||||
|
||||
logLanePhase(lane, "dashboard");
|
||||
await runDashboardSmoke({
|
||||
lane,
|
||||
logPath: join(params.logsDir, "upgrade-dashboard.log"),
|
||||
await runTimedLanePhase(lane, "dashboard", async () => {
|
||||
await runDashboardSmoke({
|
||||
lane,
|
||||
logPath: join(params.logsDir, "upgrade-dashboard.log"),
|
||||
});
|
||||
});
|
||||
|
||||
logLanePhase(lane, "agent-turn");
|
||||
const agent = await runAgentTurn({
|
||||
lane,
|
||||
env,
|
||||
label: "upgrade",
|
||||
logPath: join(params.logsDir, "upgrade-agent.log"),
|
||||
});
|
||||
const agent = await runTimedLanePhase(lane, "agent-turn", async () =>
|
||||
runAgentTurn({
|
||||
lane,
|
||||
env,
|
||||
label: "upgrade",
|
||||
logPath: join(params.logsDir, "upgrade-agent.log"),
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
status: "pass",
|
||||
@@ -888,6 +972,7 @@ async function runUpgradeLane(params) {
|
||||
dashboardStatus: "pass",
|
||||
gatewayPort: lane.gatewayPort,
|
||||
agentOutput: trimForSummary(agent.stdout),
|
||||
phaseTimings: lane.phaseTimings,
|
||||
};
|
||||
} finally {
|
||||
await runCleanup(cleanup);
|
||||
@@ -1245,6 +1330,7 @@ function createLaneState(name) {
|
||||
stateDir,
|
||||
appDataDir,
|
||||
gatewayPort: 0,
|
||||
phaseTimings: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3268,6 +3354,10 @@ async function runCommand(command, args, options) {
|
||||
let stderr = "";
|
||||
let timedOut = false;
|
||||
let settled = false;
|
||||
const startedAt = Date.now();
|
||||
let killWaitTimer = null;
|
||||
let timer = null;
|
||||
let heartbeatTimer = null;
|
||||
|
||||
const clearTimers = () => {
|
||||
if (timer) {
|
||||
@@ -3276,6 +3366,9 @@ async function runCommand(command, args, options) {
|
||||
if (killWaitTimer) {
|
||||
clearTimeout(killWaitTimer);
|
||||
}
|
||||
if (heartbeatTimer) {
|
||||
clearInterval(heartbeatTimer);
|
||||
}
|
||||
};
|
||||
|
||||
const finalize = (callback) => {
|
||||
@@ -3307,8 +3400,7 @@ async function runCommand(command, args, options) {
|
||||
child.kill(process.platform === "win32" ? undefined : "SIGKILL");
|
||||
};
|
||||
|
||||
let killWaitTimer = null;
|
||||
const timer =
|
||||
timer =
|
||||
options.timeoutMs && Number.isFinite(options.timeoutMs)
|
||||
? setTimeout(() => {
|
||||
timedOut = true;
|
||||
@@ -3327,6 +3419,20 @@ async function runCommand(command, args, options) {
|
||||
}, 15_000);
|
||||
}, options.timeoutMs)
|
||||
: null;
|
||||
heartbeatTimer =
|
||||
CROSS_OS_COMMAND_HEARTBEAT_SECONDS > 0
|
||||
? setInterval(() => {
|
||||
const elapsedSeconds = Math.floor((Date.now() - startedAt) / 1000);
|
||||
const message = `${new Date().toISOString()} still running after ${elapsedSeconds}s: ${command} ${args.join(" ")}\n`;
|
||||
logStream.write(message);
|
||||
process.stdout.write(`[release-checks] ${message}`);
|
||||
}, CROSS_OS_COMMAND_HEARTBEAT_SECONDS * 1000)
|
||||
: null;
|
||||
heartbeatTimer?.unref?.();
|
||||
|
||||
logStream.write(
|
||||
`${new Date().toISOString()} start command=${command} args=${args.join(" ")}\n`,
|
||||
);
|
||||
|
||||
child.stdout?.on("data", (chunk) => {
|
||||
const text = chunk.toString();
|
||||
@@ -3454,6 +3560,13 @@ function writeSummary(baseDir, summaryPayload) {
|
||||
result.agentOutput ? `- Agent output: \`${trimForSummary(result.agentOutput)}\`` : "",
|
||||
result.error ? `- Error: \`${trimForSummary(result.error)}\`` : "",
|
||||
].filter(Boolean);
|
||||
if (Array.isArray(result.phaseTimings) && result.phaseTimings.length > 0) {
|
||||
lines.push("", "### Phase timings");
|
||||
for (const phase of result.phaseTimings) {
|
||||
const suffix = phase.status === "pass" ? "" : ` (${phase.status})`;
|
||||
lines.push(`- \`${phase.name}\`: ${Math.round(phase.durationMs / 1000)}s${suffix}`);
|
||||
}
|
||||
}
|
||||
writeFileSync(summaryMarkdownPath, `${lines.join("\n")}\n`, "utf8");
|
||||
}
|
||||
|
||||
@@ -3522,6 +3635,23 @@ function logLanePhase(lane, phase) {
|
||||
logPhase(`lane.${lane.name}`, phase);
|
||||
}
|
||||
|
||||
async function runTimedLanePhase(lane, phase, callback) {
|
||||
const startedAt = Date.now();
|
||||
logLanePhase(lane, phase);
|
||||
try {
|
||||
const result = await callback();
|
||||
const durationMs = Date.now() - startedAt;
|
||||
lane.phaseTimings.push({ name: phase, status: "pass", durationMs });
|
||||
logPhase(`lane.${lane.name}`, `${phase}: done in ${Math.round(durationMs / 1000)}s`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const durationMs = Date.now() - startedAt;
|
||||
lane.phaseTimings.push({ name: phase, status: "fail", durationMs });
|
||||
logPhase(`lane.${lane.name}`, `${phase}: failed in ${Math.round(durationMs / 1000)}s`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function trimForSummary(value) {
|
||||
const trimmed = value.trim();
|
||||
if (trimmed.length <= 600) {
|
||||
|
||||
Reference in New Issue
Block a user