fix(upgrade): prevent version downgrades
This commit is contained in:
parent
528c05e2fa
commit
1d1d234eed
|
|
@ -28,6 +28,7 @@
|
|||
"node-pty": "1.1.0",
|
||||
"pino": "9.14.0",
|
||||
"plist": "5.0.0",
|
||||
"semver": "7.8.5",
|
||||
"smol-toml": "^1.7.0",
|
||||
"write-file-atomic": "7.0.0",
|
||||
"ws": "8.21.0",
|
||||
|
|
@ -40,6 +41,7 @@
|
|||
"@fast-check/vitest": "0.4.1",
|
||||
"@modelcontextprotocol/client": "2.0.0-beta.4",
|
||||
"@types/node": "24.10.1",
|
||||
"@types/semver": "7.7.1",
|
||||
"@types/write-file-atomic": "4.0.3",
|
||||
"@types/ws": "8.18.1",
|
||||
"@vitest/coverage-v8": "4.1.10",
|
||||
|
|
@ -1718,6 +1720,13 @@
|
|||
"undici-types": "~7.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/semver": {
|
||||
"version": "7.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz",
|
||||
"integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/unist": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
|
||||
|
|
@ -4347,7 +4356,6 @@
|
|||
"version": "7.8.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@
|
|||
"node-pty": "1.1.0",
|
||||
"pino": "9.14.0",
|
||||
"plist": "5.0.0",
|
||||
"semver": "7.8.5",
|
||||
"smol-toml": "^1.7.0",
|
||||
"write-file-atomic": "7.0.0",
|
||||
"ws": "8.21.0",
|
||||
|
|
@ -116,6 +117,7 @@
|
|||
"@fast-check/vitest": "0.4.1",
|
||||
"@modelcontextprotocol/client": "2.0.0-beta.4",
|
||||
"@types/node": "24.10.1",
|
||||
"@types/semver": "7.7.1",
|
||||
"@types/write-file-atomic": "4.0.3",
|
||||
"@types/ws": "8.18.1",
|
||||
"@vitest/coverage-v8": "4.1.10",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { basename, dirname, resolve } from "node:path";
|
|||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import { gt, valid } from "semver";
|
||||
import { z } from "zod";
|
||||
|
||||
import { PRODUCT_IDENTITY } from "../identity.js";
|
||||
|
|
@ -59,7 +60,7 @@ export type UpgradeResult =
|
|||
readonly status: "failed";
|
||||
readonly currentVersion: string;
|
||||
readonly latestVersion: string | null;
|
||||
readonly reason: "unknown-install-method" | "install";
|
||||
readonly reason: "unknown-install-method" | "version-check" | "install";
|
||||
readonly remediation: string;
|
||||
};
|
||||
|
||||
|
|
@ -70,7 +71,20 @@ export const runUpgrade = async (
|
|||
output: UpgradeOutput = "human",
|
||||
): Promise<UpgradeResult> => {
|
||||
const latestVersion = await host.latestVersion();
|
||||
if (latestVersion === currentVersion)
|
||||
const currentIsValid = valid(currentVersion) !== null;
|
||||
const latestIsValid =
|
||||
latestVersion !== undefined && valid(latestVersion) !== null;
|
||||
if (!currentIsValid || !latestIsValid)
|
||||
return {
|
||||
status: "failed",
|
||||
currentVersion,
|
||||
latestVersion: latestVersion ?? null,
|
||||
reason: "version-check",
|
||||
remediation:
|
||||
"REA could not verify a newer registry release. Check npm registry access and version metadata, then rerun rea upgrade.",
|
||||
};
|
||||
|
||||
if (!gt(latestVersion, currentVersion))
|
||||
return {
|
||||
status: "current",
|
||||
currentVersion,
|
||||
|
|
@ -100,8 +114,8 @@ export const runUpgrade = async (
|
|||
return {
|
||||
status: "upgraded",
|
||||
previousVersion: currentVersion,
|
||||
latestVersion: latestVersion ?? null,
|
||||
versionCheck: latestVersion === undefined ? "unavailable" : "available",
|
||||
latestVersion,
|
||||
versionCheck: "available",
|
||||
installMethod: "npm",
|
||||
command: UPGRADE_COMMAND,
|
||||
clientRestartRequired: true,
|
||||
|
|
|
|||
|
|
@ -61,18 +61,62 @@ describe("CLI upgrade", () => {
|
|||
expect(host.installs).toBe(1);
|
||||
});
|
||||
|
||||
it("continues with npm latest when the registry check is unavailable", async () => {
|
||||
it("does not mutate when the registry check is unavailable", async () => {
|
||||
const host = new FakeUpgradeHost();
|
||||
host.latest = undefined;
|
||||
|
||||
expect(await runUpgrade("0.5.0", host)).toMatchObject({
|
||||
status: "upgraded",
|
||||
status: "failed",
|
||||
latestVersion: null,
|
||||
versionCheck: "unavailable",
|
||||
reason: "version-check",
|
||||
});
|
||||
expect(host.installs).toBe(0);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["1.10.0", "1.9.0"],
|
||||
["1.0.0", "1.0.0-rc.1"],
|
||||
])(
|
||||
"does not downgrade installed version %s to registry version %s",
|
||||
async (currentVersion, latestVersion) => {
|
||||
const host = new FakeUpgradeHost();
|
||||
host.latest = latestVersion;
|
||||
|
||||
expect(await runUpgrade(currentVersion, host)).toEqual({
|
||||
status: "current",
|
||||
currentVersion,
|
||||
latestVersion,
|
||||
installMethod: "npm",
|
||||
});
|
||||
expect(host.installs).toBe(0);
|
||||
},
|
||||
);
|
||||
|
||||
it("upgrades a prerelease when the final release is available", async () => {
|
||||
const host = new FakeUpgradeHost();
|
||||
host.latest = "1.0.0";
|
||||
|
||||
expect(await runUpgrade("1.0.0-rc.1", host)).toMatchObject({
|
||||
status: "upgraded",
|
||||
previousVersion: "1.0.0-rc.1",
|
||||
latestVersion: "1.0.0",
|
||||
});
|
||||
expect(host.installs).toBe(1);
|
||||
});
|
||||
|
||||
it("rejects invalid registry version metadata without mutation", async () => {
|
||||
const host = new FakeUpgradeHost();
|
||||
host.latest = "latest";
|
||||
|
||||
expect(await runUpgrade("1.0.0", host)).toMatchObject({
|
||||
status: "failed",
|
||||
currentVersion: "1.0.0",
|
||||
latestVersion: "latest",
|
||||
reason: "version-check",
|
||||
});
|
||||
expect(host.installs).toBe(0);
|
||||
});
|
||||
|
||||
it("preserves stdout for an explicitly structured CLI response", async () => {
|
||||
const host = new FakeUpgradeHost();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue