mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-05-19 08:01:17 +00:00
fix(update): auto-install minor bumps, version 1.0.20 for patch delivery (#3342)
The 1.0.x → 1.1.0 minor bump blocked auto-update for all users since only patch bumps were auto-installed. Users without SPAWN_AUTO_UPDATE=1 were stuck on 1.0.x and never received the telemetry fix. Version set to 1.0.20 so existing 1.0.x users see it as a patch bump and auto-install it. The new update logic then allows future minor bumps (same major) to auto-install too. Only major bumps (2.0.0+) require SPAWN_AUTO_UPDATE=1. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3824f6d6c8
commit
75a22f2d06
3 changed files with 21 additions and 36 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@openrouter/spawn",
|
||||
"version": "1.1.1",
|
||||
"version": "1.0.20",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"spawn": "cli.js"
|
||||
|
|
|
|||
|
|
@ -486,9 +486,9 @@ describe("update-check", () => {
|
|||
// - SPAWN_NO_AUTO_UPDATE=1 suppresses auto-install entirely
|
||||
describe("update policy", () => {
|
||||
it("auto-installs patch bumps even without SPAWN_AUTO_UPDATE=1", async () => {
|
||||
// 1.1.0 -> 1.1.99 is a patch bump (same major.minor)
|
||||
// 1.0.20 -> 1.0.99 is a patch bump (same major.minor)
|
||||
process.env.SPAWN_AUTO_UPDATE = undefined;
|
||||
const fetchSpy = spyOn(global, "fetch").mockImplementation(() => Promise.resolve(new Response("1.1.99\n")));
|
||||
const fetchSpy = spyOn(global, "fetch").mockImplementation(() => Promise.resolve(new Response("1.0.99\n")));
|
||||
const { executor } = await import("../update-check.js");
|
||||
const execFileSyncSpy = spyOn(executor, "execFileSync").mockImplementation((file: string) =>
|
||||
Buffer.from(file === "curl" ? FAKE_INSTALL_SCRIPT : ""),
|
||||
|
|
@ -507,8 +507,8 @@ describe("update-check", () => {
|
|||
execFileSyncSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("shows notice only for minor bumps without SPAWN_AUTO_UPDATE=1", async () => {
|
||||
// 1.1.0 -> 1.2.0 is a minor bump
|
||||
it("auto-installs minor bumps (same major)", async () => {
|
||||
// 1.0.20 -> 1.2.0 is a minor bump — should auto-install
|
||||
process.env.SPAWN_AUTO_UPDATE = undefined;
|
||||
const fetchSpy = spyOn(global, "fetch").mockImplementation(() => Promise.resolve(new Response("1.2.0\n")));
|
||||
const { executor } = await import("../update-check.js");
|
||||
|
|
@ -519,20 +519,15 @@ describe("update-check", () => {
|
|||
const { checkForUpdates } = await import("../update-check.js");
|
||||
await checkForUpdates();
|
||||
|
||||
const output = consoleErrorSpy.mock.calls.map((call: unknown[]) => call[0]).join("\n");
|
||||
// Notice should mention the version jump
|
||||
expect(output).toContain("Update available");
|
||||
expect(output).toContain("1.2.0");
|
||||
// Must NOT auto-install — no curl, no bash, no re-exec
|
||||
expect(execFileSyncSpy).not.toHaveBeenCalled();
|
||||
expect(processExitSpy).not.toHaveBeenCalled();
|
||||
// Should auto-install: curl to fetch script, bash to run it, which + re-exec
|
||||
expect(execFileSyncSpy).toHaveBeenCalled();
|
||||
|
||||
fetchSpy.mockRestore();
|
||||
execFileSyncSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("shows notice only for major bumps without SPAWN_AUTO_UPDATE=1", async () => {
|
||||
// 1.1.0 -> 2.0.0 is a major bump
|
||||
// 1.0.20 -> 2.0.0 is a major bump — should NOT auto-install
|
||||
process.env.SPAWN_AUTO_UPDATE = undefined;
|
||||
const fetchSpy = spyOn(global, "fetch").mockImplementation(() => Promise.resolve(new Response("2.0.0\n")));
|
||||
const { executor } = await import("../update-check.js");
|
||||
|
|
@ -550,8 +545,8 @@ describe("update-check", () => {
|
|||
execFileSyncSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("auto-installs minor bumps WITH SPAWN_AUTO_UPDATE=1", async () => {
|
||||
// 1.1.0 -> 1.2.0 with opt-in env var
|
||||
it("auto-installs major bumps WITH SPAWN_AUTO_UPDATE=1", async () => {
|
||||
// 1.0.20 -> 1.2.0 with opt-in env var
|
||||
process.env.SPAWN_AUTO_UPDATE = "1";
|
||||
const fetchSpy = spyOn(global, "fetch").mockImplementation(() => Promise.resolve(new Response("1.2.0\n")));
|
||||
const { executor } = await import("../update-check.js");
|
||||
|
|
|
|||
|
|
@ -89,13 +89,6 @@ function compareVersions(current: string, latest: string): boolean {
|
|||
return false;
|
||||
}
|
||||
|
||||
/** Check if two versions share the same major.minor (e.g. 1.0.x). */
|
||||
function isSameMinor(current: string, latest: string): boolean {
|
||||
const c = parseSemver(current);
|
||||
const l = parseSemver(latest);
|
||||
return c[0] === l[0] && c[1] === l[1];
|
||||
}
|
||||
|
||||
// ── Failure Backoff ──────────────────────────────────────────────────────────
|
||||
|
||||
function isUpdateBackedOff(): boolean {
|
||||
|
|
@ -439,25 +432,22 @@ export async function checkForUpdates(jsonOutput = false): Promise<void> {
|
|||
|
||||
// Notify (or auto-install) if a newer version is available.
|
||||
if (compareVersions(VERSION, latestVersion)) {
|
||||
// Update policy, semver-aligned:
|
||||
// Update policy:
|
||||
//
|
||||
// PATCH bumps (same major.minor, e.g. 1.0.5 → 1.0.7) are always
|
||||
// auto-installed. Patches are reserved for bug fixes and security
|
||||
// hardening — users benefit from getting them without opting in, and
|
||||
// the blast radius is bounded by semver: no behavior changes, no
|
||||
// breaking changes, no new features.
|
||||
// PATCH and MINOR bumps (e.g. 1.0.5 → 1.0.7, 1.0.x → 1.1.0) are
|
||||
// auto-installed. These contain bug fixes, security hardening, and
|
||||
// new features that users benefit from getting promptly.
|
||||
//
|
||||
// MINOR / MAJOR bumps (e.g. 1.0.x → 1.1.0, 1.x.x → 2.0.0) respect
|
||||
// SPAWN_AUTO_UPDATE=1 as opt-in. These can contain behavior changes
|
||||
// and users should decide when to move to them.
|
||||
// MAJOR bumps (e.g. 1.x.x → 2.0.0) respect SPAWN_AUTO_UPDATE=1
|
||||
// as opt-in, since these can contain breaking changes.
|
||||
//
|
||||
// SPAWN_NO_AUTO_UPDATE=1 lets users opt OUT of patch-level auto-update
|
||||
// entirely if they need a fully pinned CLI (CI environments, etc.).
|
||||
const patchOnly = isSameMinor(VERSION, latestVersion);
|
||||
// SPAWN_NO_AUTO_UPDATE=1 lets users opt OUT of auto-update entirely
|
||||
// if they need a fully pinned CLI (CI environments, etc.).
|
||||
const sameMajor = parseSemver(VERSION)[0] === parseSemver(latestVersion)[0];
|
||||
const explicitOptOut = process.env.SPAWN_NO_AUTO_UPDATE === "1";
|
||||
const explicitOptIn = process.env.SPAWN_AUTO_UPDATE === "1";
|
||||
|
||||
const shouldAutoInstall = !explicitOptOut && (patchOnly || explicitOptIn);
|
||||
const shouldAutoInstall = !explicitOptOut && (sameMajor || explicitOptIn);
|
||||
|
||||
if (shouldAutoInstall) {
|
||||
const r = tryCatch(() => performAutoUpdate(latestVersion, jsonOutput));
|
||||
|
|
@ -466,7 +456,7 @@ export async function checkForUpdates(jsonOutput = false): Promise<void> {
|
|||
logDebug(getErrorMessage(r.error));
|
||||
}
|
||||
} else {
|
||||
// Minor/major bump without opt-in, or explicit opt-out — show notice.
|
||||
// Major bump without opt-in, or explicit opt-out — show notice.
|
||||
printUpdateNotice(latestVersion);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue