fix(cli,release): repair nightly updates and snapshots
This commit is contained in:
parent
19d2a34f6a
commit
035b07109b
|
|
@ -37,7 +37,7 @@
|
|||
]
|
||||
],
|
||||
"snapshot": {
|
||||
"useCalculatedVersionForSnapshots": true,
|
||||
"useCalculatedVersion": true,
|
||||
"prereleaseTemplate": "{tag}-{commit}"
|
||||
},
|
||||
"access": "public",
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ import {
|
|||
maybeShowUpdateNotice,
|
||||
scheduleBackgroundRefresh,
|
||||
isVersionOutdated,
|
||||
isOutdatedForChannel,
|
||||
resolveUpdateChannel,
|
||||
resolveInstallMethodOverride,
|
||||
isManualOnlyInstall,
|
||||
|
|
@ -115,6 +116,42 @@ describe("update-check", () => {
|
|||
mockGlobalConfig.value = null;
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// isOutdatedForChannel
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
describe("isOutdatedForChannel", () => {
|
||||
it("treats any nightly dist-tag identity change as an update in both lexical directions", () => {
|
||||
expect(isOutdatedForChannel("0.0.0-nightly-abc", "0.0.0-nightly-def", "nightly")).toBe(true);
|
||||
expect(isOutdatedForChannel("0.0.0-nightly-def", "0.0.0-nightly-abc", "nightly")).toBe(true);
|
||||
expect(
|
||||
isOutdatedForChannel("0.0.0-nightly-f00d123", "0.0.0-nightly-0dead01", "nightly"),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("does not update nightly when the exact dist-tag version is already installed", () => {
|
||||
expect(isOutdatedForChannel("0.0.0-nightly-abc", "0.0.0-nightly-abc", "nightly")).toBe(false);
|
||||
});
|
||||
|
||||
it("uses semver for stable-to-nightly channel switches", () => {
|
||||
expect(isOutdatedForChannel("0.7.0", "0.0.0-nightly-abc", "nightly")).toBe(false);
|
||||
expect(isOutdatedForChannel("0.7.0", "0.8.0-nightly-abc", "nightly")).toBe(true);
|
||||
expect(isOutdatedForChannel("0.8.0", "0.8.0-nightly-abc", "nightly")).toBe(false);
|
||||
});
|
||||
|
||||
it("treats nightly-to-stable fallback on the nightly channel as an update when the dist-tag differs", () => {
|
||||
// `fetchLatestVersion("nightly")` can fall back to `latest` if the
|
||||
// nightly dist-tag is absent; prerelease-to-stable is a normal
|
||||
// semver upgrade.
|
||||
expect(isOutdatedForChannel("0.0.0-nightly-abc", "0.8.0", "nightly")).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps stable comparisons numeric instead of lexical", () => {
|
||||
expect(isOutdatedForChannel("0.9.0", "0.10.0", "stable")).toBe(true);
|
||||
expect(isOutdatedForChannel("0.7.0", "0.8.0", "stable")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// isVersionOutdated
|
||||
// -----------------------------------------------------------------------
|
||||
|
|
@ -1186,24 +1223,14 @@ describe("update-check", () => {
|
|||
|
||||
describe("getUpdateCommand — channel-aware (Section B)", () => {
|
||||
it("uses @nightly tag for nightly channel", () => {
|
||||
expect(getUpdateCommand("npm-global", "nightly")).toBe(
|
||||
"npm install -g @aoagents/ao@nightly",
|
||||
);
|
||||
expect(getUpdateCommand("pnpm-global", "nightly")).toBe(
|
||||
"pnpm add -g @aoagents/ao@nightly",
|
||||
);
|
||||
expect(getUpdateCommand("bun-global", "nightly")).toBe(
|
||||
"bun add -g @aoagents/ao@nightly",
|
||||
);
|
||||
expect(getUpdateCommand("npm-global", "nightly")).toBe("npm install -g @aoagents/ao@nightly");
|
||||
expect(getUpdateCommand("pnpm-global", "nightly")).toBe("pnpm add -g @aoagents/ao@nightly");
|
||||
expect(getUpdateCommand("bun-global", "nightly")).toBe("bun add -g @aoagents/ao@nightly");
|
||||
});
|
||||
|
||||
it("uses @latest tag for stable + manual channels", () => {
|
||||
expect(getUpdateCommand("npm-global", "stable")).toBe(
|
||||
"npm install -g @aoagents/ao@latest",
|
||||
);
|
||||
expect(getUpdateCommand("npm-global", "manual")).toBe(
|
||||
"npm install -g @aoagents/ao@latest",
|
||||
);
|
||||
expect(getUpdateCommand("npm-global", "stable")).toBe("npm install -g @aoagents/ao@latest");
|
||||
expect(getUpdateCommand("npm-global", "manual")).toBe("npm install -g @aoagents/ao@latest");
|
||||
});
|
||||
|
||||
it("returns the brew upgrade notice for homebrew installs", () => {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { promisify } from "node:util";
|
|||
import {
|
||||
getInstalledAoVersion,
|
||||
getUpdateCheckCachePath,
|
||||
isVersionOutdatedForChannel as coreIsVersionOutdatedForChannel,
|
||||
isVersionOutdated as coreIsVersionOutdated,
|
||||
loadGlobalConfig,
|
||||
type UpdateChannel,
|
||||
|
|
@ -181,8 +182,7 @@ export function classifyInstallPath(resolvedPath: string): InstallMethod {
|
|||
if (isPnpmGlobal) return "pnpm-global";
|
||||
|
||||
const isNpmGlobal =
|
||||
resolvedPath.includes("/lib/node_modules/") ||
|
||||
resolvedPath.includes("\\lib\\node_modules\\");
|
||||
resolvedPath.includes("/lib/node_modules/") || resolvedPath.includes("\\lib\\node_modules\\");
|
||||
if (isNpmGlobal) return "npm-global";
|
||||
|
||||
return "unknown";
|
||||
|
|
@ -256,10 +256,7 @@ export function getGitUpdateRef(): string {
|
|||
* a notice so the user runs it themselves — auto-running `npm install -g`
|
||||
* inside a brew prefix overwrites brew's symlinks.
|
||||
*/
|
||||
export function getUpdateCommand(
|
||||
method: InstallMethod,
|
||||
channel: UpdateChannel = "stable",
|
||||
): string {
|
||||
export function getUpdateCommand(method: InstallMethod, channel: UpdateChannel = "stable"): string {
|
||||
// "manual" channel maps to "stable" for the install command — the channel
|
||||
// affects when we check, not which tag manual installers should pick.
|
||||
const tag = channel === "nightly" ? "nightly" : "latest";
|
||||
|
|
@ -284,6 +281,14 @@ export function isManualOnlyInstall(method: InstallMethod): boolean {
|
|||
return method === "homebrew";
|
||||
}
|
||||
|
||||
export function isOutdatedForChannel(
|
||||
currentVersion: string,
|
||||
latestVersion: string,
|
||||
channel: UpdateChannel,
|
||||
): boolean {
|
||||
return coreIsVersionOutdatedForChannel(currentVersion, latestVersion, channel);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Cache
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -493,7 +498,7 @@ export async function checkForUpdate(opts?: {
|
|||
isOutdated:
|
||||
cached.installMethod === "git"
|
||||
? cached.isOutdated === true
|
||||
: isVersionOutdated(currentVersion, cached.latestVersion),
|
||||
: isOutdatedForChannel(currentVersion, cached.latestVersion, channel),
|
||||
installMethod,
|
||||
recommendedCommand,
|
||||
checkedAt: cached.checkedAt,
|
||||
|
|
@ -540,14 +545,16 @@ export async function checkForUpdate(opts?: {
|
|||
currentVersionAtCheck: currentVersion,
|
||||
installMethod,
|
||||
channel,
|
||||
isOutdated: isVersionOutdated(currentVersion, latestVersion),
|
||||
isOutdated: isOutdatedForChannel(currentVersion, latestVersion, channel),
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
currentVersion,
|
||||
latestVersion,
|
||||
isOutdated: latestVersion ? isVersionOutdated(currentVersion, latestVersion) : false,
|
||||
isOutdated: latestVersion
|
||||
? isOutdatedForChannel(currentVersion, latestVersion, channel)
|
||||
: false,
|
||||
installMethod,
|
||||
recommendedCommand,
|
||||
checkedAt: latestVersion ? now : null,
|
||||
|
|
@ -585,7 +592,7 @@ export function maybeShowUpdateNotice(): void {
|
|||
const isOutdated =
|
||||
installMethod === "git"
|
||||
? cached.isOutdated === true
|
||||
: isVersionOutdated(currentVersion, cached.latestVersion);
|
||||
: isOutdatedForChannel(currentVersion, cached.latestVersion, channel);
|
||||
if (!isOutdated) return;
|
||||
|
||||
const channelSuffix = channel === "nightly" ? " (nightly)" : "";
|
||||
|
|
|
|||
|
|
@ -1,12 +1,47 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
|
||||
import { isVersionOutdated } from "../version-compare.js";
|
||||
import { isVersionOutdated, isVersionOutdatedForChannel } from "../version-compare.js";
|
||||
|
||||
// Both `packages/cli/src/lib/update-check.ts` and
|
||||
// `packages/web/src/app/api/version/route.ts` import this implementation
|
||||
// from core. These tests are the single source of truth for the comparison
|
||||
// rules — if either consumer's behavior diverges, fix the consumer, not this.
|
||||
|
||||
describe("isVersionOutdatedForChannel", () => {
|
||||
it("treats nightly dist-tag changes as updates by identity in both lexical directions", () => {
|
||||
expect(isVersionOutdatedForChannel("0.0.0-nightly-abc", "0.0.0-nightly-def", "nightly")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(isVersionOutdatedForChannel("0.0.0-nightly-def", "0.0.0-nightly-abc", "nightly")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
isVersionOutdatedForChannel("0.0.0-nightly-f00d123", "0.0.0-nightly-0dead01", "nightly"),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("does not update nightly when the exact dist-tag version is already installed", () => {
|
||||
expect(isVersionOutdatedForChannel("0.0.0-nightly-abc", "0.0.0-nightly-abc", "nightly")).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("uses semver for stable-to-nightly channel switches", () => {
|
||||
expect(isVersionOutdatedForChannel("0.7.0", "0.0.0-nightly-abc", "nightly")).toBe(false);
|
||||
expect(isVersionOutdatedForChannel("0.7.0", "0.8.0-nightly-abc", "nightly")).toBe(true);
|
||||
expect(isVersionOutdatedForChannel("0.8.0", "0.8.0-nightly-abc", "nightly")).toBe(false);
|
||||
});
|
||||
|
||||
it("treats nightly-to-stable fallback on the nightly channel as an update when the dist-tag differs", () => {
|
||||
expect(isVersionOutdatedForChannel("0.0.0-nightly-abc", "0.8.0", "nightly")).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps stable comparisons numeric instead of lexical", () => {
|
||||
expect(isVersionOutdatedForChannel("0.9.0", "0.10.0", "stable")).toBe(true);
|
||||
expect(isVersionOutdatedForChannel("0.7.0", "0.8.0", "stable")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isVersionOutdated (shared core implementation)", () => {
|
||||
describe("numeric major/minor/patch", () => {
|
||||
it("treats lower major as older", () => {
|
||||
|
|
|
|||
|
|
@ -425,7 +425,7 @@ export { UpdateChannelSchema, InstallMethodOverrideSchema } from "./global-confi
|
|||
|
||||
// Channel-aware semver comparison shared by the CLI's update-check and the
|
||||
// dashboard's /api/version route.
|
||||
export { isVersionOutdated } from "./version-compare.js";
|
||||
export { isVersionOutdated, isVersionOutdatedForChannel } from "./version-compare.js";
|
||||
|
||||
// Cache-layer primitives for the update pipeline. Both the CLI and the
|
||||
// dashboard's /api/version route read the same cache file; centralising the
|
||||
|
|
|
|||
|
|
@ -8,6 +8,34 @@
|
|||
* Spec: release-process.html §07 (Auto-update mechanics).
|
||||
*/
|
||||
|
||||
import type { UpdateChannel } from "./global-config.js";
|
||||
|
||||
export function isVersionOutdatedForChannel(
|
||||
current: string,
|
||||
latest: string,
|
||||
channel: UpdateChannel,
|
||||
): boolean {
|
||||
if (channel === "nightly") {
|
||||
const currentParsed = parseVersion(current);
|
||||
const latestParsed = parseVersion(latest);
|
||||
if (
|
||||
isNightlyPrerelease(currentParsed.prerelease) &&
|
||||
isNightlyPrerelease(latestParsed.prerelease)
|
||||
) {
|
||||
return current !== latest;
|
||||
}
|
||||
}
|
||||
return isVersionOutdated(current, latest);
|
||||
}
|
||||
|
||||
function isNightlyPrerelease(prerelease: string | undefined): boolean {
|
||||
return (
|
||||
prerelease === "nightly" ||
|
||||
prerelease?.startsWith("nightly-") === true ||
|
||||
prerelease?.startsWith("nightly.") === true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if `current` is an older version than `latest`.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
import { NextResponse } from "next/server";
|
||||
import {
|
||||
getInstalledAoVersion,
|
||||
isVersionOutdated,
|
||||
isVersionOutdatedForChannel,
|
||||
loadGlobalConfig,
|
||||
readUpdateCheckCacheRaw,
|
||||
type UpdateChannel,
|
||||
|
|
@ -53,7 +53,8 @@ export async function GET() {
|
|||
const latest = cache?.latestVersion && cacheMatchesChannel ? cache.latestVersion : null;
|
||||
|
||||
// Git installs cache `latestVersion: "origin/main"` (a ref, not a semver),
|
||||
// so `isVersionOutdated(current, "origin/main")` would always return false.
|
||||
// so `isVersionOutdatedForChannel(current, "origin/main", channel)` would
|
||||
// always return false.
|
||||
// The CLI works around this by trusting the precomputed `cached.isOutdated`
|
||||
// for git installs — mirror that here so the dashboard banner actually
|
||||
// appears when a git-installed user is behind origin/main.
|
||||
|
|
@ -62,7 +63,7 @@ export async function GET() {
|
|||
isOutdated =
|
||||
cache?.installMethod === "git"
|
||||
? cache.isOutdated === true
|
||||
: isVersionOutdated(current, latest);
|
||||
: isVersionOutdatedForChannel(current, latest, channel);
|
||||
}
|
||||
|
||||
const body: VersionResponse = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue