mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-05-19 08:01:17 +00:00
fix: auto-update silently fails in multiple scenarios (#1725)
- Prevent recursive update check during install (SPAWN_NO_UPDATE_CHECK=1) - Increase fetch timeout from 5s to 10s for slow/cold connections - Add 1-hour failure backoff to avoid repeated failed update attempts - Bump CLI version to 0.6.6 Co-authored-by: Claude <claude@anthropic.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f2010ce3bd
commit
5192912b68
3 changed files with 61 additions and 3 deletions
|
|
@ -99,7 +99,7 @@ ensure_in_path() {
|
|||
local install_dir="$1"
|
||||
if echo "${PATH}" | tr ':' '\n' | grep -qx "${install_dir}"; then
|
||||
echo ""
|
||||
"${install_dir}/spawn" version
|
||||
SPAWN_NO_UPDATE_CHECK=1 "${install_dir}/spawn" version
|
||||
echo ""
|
||||
printf "${GREEN}[spawn]${NC} Run ${BOLD}spawn${NC} to get started\n"
|
||||
else
|
||||
|
|
|
|||
|
|
@ -1,7 +1,18 @@
|
|||
import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from "bun:test";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
// ── Test Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
/** Remove the .update-failed backoff file so it doesn't interfere with tests */
|
||||
function clearUpdateBackoff() {
|
||||
try {
|
||||
fs.unlinkSync(path.join(process.env.HOME || "/tmp", ".config", "spawn", ".update-failed"));
|
||||
} catch {
|
||||
// File may not exist
|
||||
}
|
||||
}
|
||||
|
||||
function mockEnv() {
|
||||
const originalEnv = { ...process.env };
|
||||
process.env.NODE_ENV = undefined;
|
||||
|
|
@ -23,6 +34,7 @@ describe("update-check", () => {
|
|||
|
||||
beforeEach(() => {
|
||||
originalEnv = mockEnv();
|
||||
clearUpdateBackoff();
|
||||
consoleErrorSpy = spyOn(console, "error").mockImplementation(() => {});
|
||||
// Mock process.exit to prevent tests from exiting
|
||||
processExitSpy = spyOn(process, "exit").mockImplementation((() => {}) as any);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import "./unicode-detect.js"; // Ensure TERM is set before using symbols
|
||||
import { execSync as nodeExecSync, execFileSync as nodeExecFileSync, type ExecSyncOptions, type ExecFileSyncOptions } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import pc from "picocolors";
|
||||
import pkg from "../package.json" with { type: "json" };
|
||||
import { RAW_BASE } from "./manifest.js";
|
||||
|
|
@ -14,7 +16,8 @@ export const executor = {
|
|||
|
||||
// ── Constants ──────────────────────────────────────────────────────────────────
|
||||
|
||||
const FETCH_TIMEOUT = 5000; // 5 seconds
|
||||
const FETCH_TIMEOUT = 10000; // 10 seconds
|
||||
const UPDATE_BACKOFF_MS = 60 * 60 * 1000; // 1 hour
|
||||
|
||||
// Validate RAW_BASE matches expected GitHub raw content URL pattern (defense-in-depth, CWE-78)
|
||||
const GITHUB_RAW_URL_PATTERN = /^https:\/\/raw\.githubusercontent\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
||||
|
|
@ -59,6 +62,42 @@ function compareVersions(current: string, latest: string): boolean {
|
|||
return false; // Versions are equal
|
||||
}
|
||||
|
||||
// ── Failure Backoff ──────────────────────────────────────────────────────────
|
||||
|
||||
function getUpdateFailedPath(): string {
|
||||
return path.join(process.env.HOME || "/tmp", ".config", "spawn", ".update-failed");
|
||||
}
|
||||
|
||||
export function isUpdateBackedOff(): boolean {
|
||||
try {
|
||||
const failedPath = getUpdateFailedPath();
|
||||
const content = fs.readFileSync(failedPath, "utf8").trim();
|
||||
const failedAt = parseInt(content, 10);
|
||||
if (isNaN(failedAt)) return false;
|
||||
return Date.now() - failedAt < UPDATE_BACKOFF_MS;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function markUpdateFailed(): void {
|
||||
try {
|
||||
const failedPath = getUpdateFailedPath();
|
||||
fs.mkdirSync(path.dirname(failedPath), { recursive: true });
|
||||
fs.writeFileSync(failedPath, String(Date.now()));
|
||||
} catch {
|
||||
// Best-effort — don't break the CLI if we can't write the file
|
||||
}
|
||||
}
|
||||
|
||||
function clearUpdateFailed(): void {
|
||||
try {
|
||||
fs.unlinkSync(getUpdateFailedPath());
|
||||
} catch {
|
||||
// File may not exist — that's fine
|
||||
}
|
||||
}
|
||||
|
||||
/** Print boxed update banner to stderr */
|
||||
function printUpdateBanner(latestVersion: string): void {
|
||||
const line1 = `Update available: v${VERSION} -> v${latestVersion}`;
|
||||
|
|
@ -143,8 +182,10 @@ function performAutoUpdate(latestVersion: string): void {
|
|||
|
||||
console.error();
|
||||
console.error(pc.green(pc.bold(`${CHECK_MARK} Updated successfully!`)));
|
||||
clearUpdateFailed();
|
||||
reExecWithArgs();
|
||||
} catch {
|
||||
markUpdateFailed();
|
||||
console.error();
|
||||
console.error(pc.red(pc.bold(`${CROSS_MARK} Auto-update failed`)));
|
||||
console.error(pc.dim(" Please update manually:"));
|
||||
|
|
@ -159,7 +200,7 @@ function performAutoUpdate(latestVersion: string): void {
|
|||
|
||||
/**
|
||||
* Check for updates on every run and auto-update if available.
|
||||
* Uses a 5-second timeout to avoid blocking for too long.
|
||||
* Uses a 10-second timeout to avoid blocking for too long.
|
||||
*/
|
||||
export async function checkForUpdates(): Promise<void> {
|
||||
// Skip in test environment
|
||||
|
|
@ -172,6 +213,11 @@ export async function checkForUpdates(): Promise<void> {
|
|||
return;
|
||||
}
|
||||
|
||||
// Skip if a recent auto-update failed (backoff for 1 hour)
|
||||
if (isUpdateBackedOff()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Always fetch the latest version on every run
|
||||
try {
|
||||
const latestVersion = await fetchLatestVersion();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue