Skip to content

Commit 5a4ab2b

Browse files
authored
Fix upgrade version detection (#11829)
1 parent b00c260 commit 5a4ab2b

File tree

3 files changed

+86
-15
lines changed

3 files changed

+86
-15
lines changed

apps/cli/install.sh

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,60 @@ get_version() {
104104
error "Failed to fetch releases from GitHub. Check your internet connection."
105105
}
106106

107-
# Extract the latest cli-v* tag
108-
VERSION=$(echo "$RELEASES_JSON" |
109-
grep -o '"tag_name": "cli-v[^"]*"' |
110-
head -1 |
111-
sed 's/"tag_name": "cli-v//' |
112-
sed 's/"//')
107+
# Extract highest cli-v* tag by semantic version (do not rely on API ordering)
108+
VERSION=$(printf "%s" "$RELEASES_JSON" | node -e '
109+
const fs = require("fs")
110+
const input = fs.readFileSync(0, "utf8")
111+
let releases
112+
try {
113+
releases = JSON.parse(input)
114+
} catch {
115+
process.exit(1)
116+
}
117+
118+
function parseVersion(version) {
119+
const core = String(version).trim().split("+", 1)[0].split("-", 1)[0]
120+
if (!core) return null
121+
const parts = core.split(".")
122+
if (parts.length === 0 || parts.some((part) => !/^\d+$/.test(part))) {
123+
return null
124+
}
125+
return parts.map((part) => Number.parseInt(part, 10))
126+
}
127+
128+
function compareVersions(a, b) {
129+
const maxLength = Math.max(a.length, b.length)
130+
for (let i = 0; i < maxLength; i++) {
131+
const aPart = a[i] ?? 0
132+
const bPart = b[i] ?? 0
133+
if (aPart > bPart) return 1
134+
if (aPart < bPart) return -1
135+
}
136+
return 0
137+
}
138+
139+
let latestVersion = ""
140+
let latestParts = null
141+
142+
if (Array.isArray(releases)) {
143+
for (const release of releases) {
144+
if (!release || typeof release.tag_name !== "string" || !release.tag_name.startsWith("cli-v")) {
145+
continue
146+
}
147+
const candidate = release.tag_name.slice("cli-v".length)
148+
const candidateParts = parseVersion(candidate)
149+
if (!candidateParts) continue
150+
if (!latestParts || compareVersions(candidateParts, latestParts) > 0) {
151+
latestVersion = candidate
152+
latestParts = candidateParts
153+
}
154+
}
155+
}
156+
157+
if (latestVersion) {
158+
process.stdout.write(latestVersion)
159+
}
160+
')
113161

114162
if [ -z "$VERSION" ]; then
115163
error "Could not find any CLI releases. The CLI may not have been released yet."

apps/cli/src/commands/cli/__tests__/upgrade.test.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,23 @@ describe("compareVersions", () => {
2626
expect(compareVersions("cli-v1.2.3", "1.2.2")).toBe(1)
2727
expect(compareVersions("1.2.3-beta.1", "1.2.3")).toBe(0)
2828
})
29+
30+
it("compares multi-digit patch versions numerically", () => {
31+
expect(compareVersions("0.1.10", "0.1.9")).toBe(1)
32+
})
2933
})
3034

3135
describe("getLatestCliVersion", () => {
32-
it("returns the first cli-v release tag from GitHub releases", async () => {
36+
it("returns the highest cli-v release tag from GitHub releases", async () => {
3337
const fetchImpl = (async () =>
3438
createFetchResponse([
39+
{ tag_name: "cli-v0.1.9" },
3540
{ tag_name: "v9.9.9" },
36-
{ tag_name: "cli-v0.3.1" },
37-
{ tag_name: "cli-v0.3.0" },
41+
{ tag_name: "cli-v0.1.10" },
42+
{ tag_name: "cli-v0.1.8" },
3843
])) as typeof fetch
3944

40-
await expect(getLatestCliVersion(fetchImpl)).resolves.toBe("0.3.1")
45+
await expect(getLatestCliVersion(fetchImpl)).resolves.toBe("0.1.10")
4146
})
4247

4348
it("throws when release check fails", async () => {

apps/cli/src/commands/cli/upgrade.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,23 +82,37 @@ export async function getLatestCliVersion(fetchImpl: typeof fetch = fetch): Prom
8282
throw new Error("Invalid release response from GitHub.")
8383
}
8484

85+
let latestVersion: string | undefined
86+
8587
for (const release of releases) {
8688
if (!isRecord(release)) {
8789
continue
8890
}
8991

9092
const tagName = release.tag_name
9193
if (typeof tagName === "string" && tagName.startsWith("cli-v")) {
92-
return tagName.slice("cli-v".length)
94+
const candidate = tagName.slice("cli-v".length)
95+
try {
96+
if (!latestVersion || compareVersions(candidate, latestVersion) > 0) {
97+
latestVersion = candidate
98+
}
99+
} catch {
100+
// Ignore malformed CLI tags and keep scanning other releases.
101+
}
93102
}
94103
}
95104

105+
if (latestVersion) {
106+
return latestVersion
107+
}
108+
96109
throw new Error("Could not determine the latest CLI release version.")
97110
}
98111

99-
export function runUpgradeInstaller(spawnImpl: typeof spawn = spawn): Promise<void> {
112+
export function runUpgradeInstaller(version?: string, spawnImpl: typeof spawn = spawn): Promise<void> {
100113
return new Promise((resolve, reject) => {
101-
const child = spawnImpl("sh", ["-c", INSTALL_SCRIPT_COMMAND], { stdio: "inherit" })
114+
const env = version ? { ...process.env, ROO_VERSION: version } : process.env
115+
const child = spawnImpl("sh", ["-c", INSTALL_SCRIPT_COMMAND], { stdio: "inherit", env })
102116

103117
child.once("error", (error) => {
104118
reject(error)
@@ -119,7 +133,7 @@ export function runUpgradeInstaller(spawnImpl: typeof spawn = spawn): Promise<vo
119133
export async function upgrade(options: UpgradeOptions = {}): Promise<void> {
120134
const currentVersion = options.currentVersion ?? VERSION
121135
const fetchImpl = options.fetchImpl ?? fetch
122-
const runInstaller = options.runInstaller ?? (() => runUpgradeInstaller())
136+
const runInstaller = options.runInstaller
123137

124138
console.log(`Current version: ${currentVersion}`)
125139

@@ -132,6 +146,10 @@ export async function upgrade(options: UpgradeOptions = {}): Promise<void> {
132146
}
133147

134148
console.log(`Upgrading Roo CLI from ${currentVersion} to ${latestVersion}...`)
135-
await runInstaller()
149+
if (runInstaller) {
150+
await runInstaller()
151+
} else {
152+
await runUpgradeInstaller(latestVersion)
153+
}
136154
console.log("✓ Upgrade completed.")
137155
}

0 commit comments

Comments
 (0)