From 3d5ffee07fa78b7d7b3f9bdb13df81b861f9960f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 20 Jan 2026 16:28:25 +0000 Subject: [PATCH] fix: prefer stable release when beta lags --- src/cli/update-cli.ts | 11 +++++++++-- src/infra/update-check.ts | 25 +++++++++++++++++++++++++ src/infra/update-runner.ts | 13 +++++++++++-- src/infra/update-startup.ts | 22 +++++++++------------- 4 files changed, 54 insertions(+), 17 deletions(-) diff --git a/src/cli/update-cli.ts b/src/cli/update-cli.ts index 76d526cc4..3560c0511 100644 --- a/src/cli/update-cli.ts +++ b/src/cli/update-cli.ts @@ -9,6 +9,7 @@ import { checkUpdateStatus, compareSemverStrings, fetchNpmTagVersion, + resolveNpmChannelTag, } from "../infra/update-check.js"; import { parseSemver } from "../infra/runtime-guard.js"; import { @@ -411,10 +412,16 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise { const gitCheckout = await isGitCheckout(root); const defaultChannel = gitCheckout ? DEFAULT_GIT_CHANNEL : DEFAULT_PACKAGE_CHANNEL; const channel = requestedChannel ?? storedChannel ?? defaultChannel; - const tag = normalizeTag(opts.tag) ?? channelToNpmTag(channel); + const explicitTag = normalizeTag(opts.tag); + let tag = explicitTag ?? channelToNpmTag(channel); if (!gitCheckout) { const currentVersion = await readPackageVersion(root); - const targetVersion = await resolveTargetVersion(tag, timeoutMs); + const targetVersion = explicitTag + ? await resolveTargetVersion(tag, timeoutMs) + : await resolveNpmChannelTag({ channel, timeoutMs }).then((resolved) => { + tag = resolved.tag; + return resolved.version; + }); const cmp = currentVersion && targetVersion ? compareSemverStrings(currentVersion, targetVersion) : null; const needsConfirm = diff --git a/src/infra/update-check.ts b/src/infra/update-check.ts index 603230040..2e020ff8d 100644 --- a/src/infra/update-check.ts +++ b/src/infra/update-check.ts @@ -3,6 +3,7 @@ import path from "node:path"; import { runCommandWithTimeout } from "../process/exec.js"; import { parseSemver } from "./runtime-guard.js"; +import { channelToNpmTag, type UpdateChannel } from "./update-channels.js"; export type PackageManager = "pnpm" | "bun" | "npm" | "unknown"; @@ -315,6 +316,30 @@ export async function fetchNpmTagVersion(params: { } } +export async function resolveNpmChannelTag(params: { + channel: UpdateChannel; + timeoutMs?: number; +}): Promise<{ tag: string; version: string | null }> { + const channelTag = channelToNpmTag(params.channel); + const channelStatus = await fetchNpmTagVersion({ tag: channelTag, timeoutMs: params.timeoutMs }); + if (params.channel !== "beta") { + return { tag: channelTag, version: channelStatus.version }; + } + + const latestStatus = await fetchNpmTagVersion({ tag: "latest", timeoutMs: params.timeoutMs }); + if (!latestStatus.version) { + return { tag: channelTag, version: channelStatus.version }; + } + if (!channelStatus.version) { + return { tag: "latest", version: latestStatus.version }; + } + const cmp = compareSemverStrings(channelStatus.version, latestStatus.version); + if (cmp != null && cmp < 0) { + return { tag: "latest", version: latestStatus.version }; + } + return { tag: channelTag, version: channelStatus.version }; +} + export function compareSemverStrings(a: string | null, b: string | null): number | null { const pa = parseSemver(a); const pb = parseSemver(b); diff --git a/src/infra/update-runner.ts b/src/infra/update-runner.ts index 1ca9adf1c..324eccc01 100644 --- a/src/infra/update-runner.ts +++ b/src/infra/update-runner.ts @@ -3,6 +3,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { type CommandOptions, runCommandWithTimeout } from "../process/exec.js"; +import { compareSemverStrings } from "./update-check.js"; import { DEV_BRANCH, isBetaTag, isStableTag, type UpdateChannel } from "./update-channels.js"; import { trimLogTail } from "./restart-sentinel.js"; @@ -143,8 +144,16 @@ async function resolveChannelTag( channel: Exclude, ): Promise { const tags = await listGitTags(runCommand, root, timeoutMs); - const predicate = channel === "beta" ? isBetaTag : isStableTag; - return tags.find((tag) => predicate(tag)) ?? null; + if (channel === "beta") { + const betaTag = tags.find((tag) => isBetaTag(tag)) ?? null; + const stableTag = tags.find((tag) => isStableTag(tag)) ?? null; + if (!betaTag) return stableTag; + if (!stableTag) return betaTag; + const cmp = compareSemverStrings(betaTag, stableTag); + if (cmp != null && cmp < 0) return stableTag; + return betaTag; + } + return tags.find((tag) => isStableTag(tag)) ?? null; } async function resolveGitRoot( diff --git a/src/infra/update-startup.ts b/src/infra/update-startup.ts index 48facd2c5..62425e48c 100644 --- a/src/infra/update-startup.ts +++ b/src/infra/update-startup.ts @@ -4,12 +4,8 @@ import path from "node:path"; import type { loadConfig } from "../config/config.js"; import { resolveStateDir } from "../config/paths.js"; import { resolveClawdbotPackageRoot } from "./clawdbot-root.js"; -import { compareSemverStrings, fetchNpmTagVersion, checkUpdateStatus } from "./update-check.js"; -import { - channelToNpmTag, - normalizeUpdateChannel, - DEFAULT_PACKAGE_CHANNEL, -} from "./update-channels.js"; +import { compareSemverStrings, resolveNpmChannelTag, checkUpdateStatus } from "./update-check.js"; +import { normalizeUpdateChannel, DEFAULT_PACKAGE_CHANNEL } from "./update-channels.js"; import { VERSION } from "../version.js"; import { formatCliCommand } from "../cli/command-format.js"; @@ -84,22 +80,22 @@ export async function runGatewayUpdateCheck(params: { } const channel = normalizeUpdateChannel(params.cfg.update?.channel) ?? DEFAULT_PACKAGE_CHANNEL; - const tag = channelToNpmTag(channel); - const tagStatus = await fetchNpmTagVersion({ tag, timeoutMs: 2500 }); - if (!tagStatus.version) { + const resolved = await resolveNpmChannelTag({ channel, timeoutMs: 2500 }); + const tag = resolved.tag; + if (!resolved.version) { await writeState(statePath, nextState); return; } - const cmp = compareSemverStrings(VERSION, tagStatus.version); + const cmp = compareSemverStrings(VERSION, resolved.version); if (cmp != null && cmp < 0) { const shouldNotify = - state.lastNotifiedVersion !== tagStatus.version || state.lastNotifiedTag !== tag; + state.lastNotifiedVersion !== resolved.version || state.lastNotifiedTag !== tag; if (shouldNotify) { params.log.info( - `update available (${tag}): v${tagStatus.version} (current v${VERSION}). Run: ${formatCliCommand("clawdbot update")}`, + `update available (${tag}): v${resolved.version} (current v${VERSION}). Run: ${formatCliCommand("clawdbot update")}`, ); - nextState.lastNotifiedVersion = tagStatus.version; + nextState.lastNotifiedVersion = resolved.version; nextState.lastNotifiedTag = tag; } }