fix: prefer stable release when beta lags

This commit is contained in:
Peter Steinberger
2026-01-20 16:28:25 +00:00
parent 4fda10c508
commit 3d5ffee07f
4 changed files with 54 additions and 17 deletions

View File

@@ -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<void> {
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 =

View File

@@ -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);

View File

@@ -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<UpdateChannel, "dev">,
): Promise<string | null> {
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(

View File

@@ -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;
}
}