fix(update): auto-install minor bumps, version 1.0.20 for patch delivery (#3342)
Some checks failed
CLI Release / Build and release CLI (push) Has been cancelled
Lint / ShellCheck (push) Has been cancelled
Lint / Biome Lint (push) Has been cancelled
Lint / macOS Compatibility (push) Has been cancelled

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:
Ahmed Abushagur 2026-04-22 14:07:41 -07:00 committed by GitHub
parent 3824f6d6c8
commit 75a22f2d06
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 21 additions and 36 deletions

View file

@ -1,6 +1,6 @@
{
"name": "@openrouter/spawn",
"version": "1.1.1",
"version": "1.0.20",
"type": "module",
"bin": {
"spawn": "cli.js"

View file

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

View file

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