fix(auto-update): revert config pin on install failure to prevent version mismatch
When bun install fails after updating the config pin, the config now shows the new version but the actual package is the old one. Add revertPinnedVersion() to roll back the config entry on install failure, keeping config and installed version in sync. Ref #1472
This commit is contained in:
parent
b0c570e054
commit
67b4665c28
@ -3,6 +3,6 @@ export { getLocalDevVersion } from "./checker/local-dev-version"
|
||||
export { findPluginEntry } from "./checker/plugin-entry"
|
||||
export type { PluginEntryInfo } from "./checker/plugin-entry"
|
||||
export { getCachedVersion } from "./checker/cached-version"
|
||||
export { updatePinnedVersion } from "./checker/pinned-version-updater"
|
||||
export { updatePinnedVersion, revertPinnedVersion } from "./checker/pinned-version-updater"
|
||||
export { getLatestVersion } from "./checker/latest-version"
|
||||
export { checkForUpdate } from "./checker/check-for-update"
|
||||
|
||||
@ -0,0 +1,133 @@
|
||||
import { describe, test, expect, beforeEach, afterEach } from "bun:test"
|
||||
import * as fs from "node:fs"
|
||||
import * as path from "node:path"
|
||||
import * as os from "node:os"
|
||||
import { updatePinnedVersion, revertPinnedVersion } from "./pinned-version-updater"
|
||||
|
||||
describe("pinned-version-updater", () => {
|
||||
let tmpDir: string
|
||||
let configPath: string
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "omo-updater-test-"))
|
||||
configPath = path.join(tmpDir, "opencode.json")
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
describe("updatePinnedVersion", () => {
|
||||
test("updates pinned version in config", () => {
|
||||
//#given
|
||||
const config = JSON.stringify({
|
||||
plugin: ["oh-my-opencode@3.1.8"],
|
||||
})
|
||||
fs.writeFileSync(configPath, config)
|
||||
|
||||
//#when
|
||||
const result = updatePinnedVersion(configPath, "oh-my-opencode@3.1.8", "3.4.0")
|
||||
|
||||
//#then
|
||||
expect(result).toBe(true)
|
||||
const updated = fs.readFileSync(configPath, "utf-8")
|
||||
expect(updated).toContain("oh-my-opencode@3.4.0")
|
||||
expect(updated).not.toContain("oh-my-opencode@3.1.8")
|
||||
})
|
||||
|
||||
test("returns false when entry not found", () => {
|
||||
//#given
|
||||
const config = JSON.stringify({
|
||||
plugin: ["some-other-plugin"],
|
||||
})
|
||||
fs.writeFileSync(configPath, config)
|
||||
|
||||
//#when
|
||||
const result = updatePinnedVersion(configPath, "oh-my-opencode@3.1.8", "3.4.0")
|
||||
|
||||
//#then
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
test("returns false when no plugin array exists", () => {
|
||||
//#given
|
||||
const config = JSON.stringify({ agent: {} })
|
||||
fs.writeFileSync(configPath, config)
|
||||
|
||||
//#when
|
||||
const result = updatePinnedVersion(configPath, "oh-my-opencode@3.1.8", "3.4.0")
|
||||
|
||||
//#then
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("revertPinnedVersion", () => {
|
||||
test("reverts from failed version back to original entry", () => {
|
||||
//#given
|
||||
const config = JSON.stringify({
|
||||
plugin: ["oh-my-opencode@3.4.0"],
|
||||
})
|
||||
fs.writeFileSync(configPath, config)
|
||||
|
||||
//#when
|
||||
const result = revertPinnedVersion(configPath, "3.4.0", "oh-my-opencode@3.1.8")
|
||||
|
||||
//#then
|
||||
expect(result).toBe(true)
|
||||
const reverted = fs.readFileSync(configPath, "utf-8")
|
||||
expect(reverted).toContain("oh-my-opencode@3.1.8")
|
||||
expect(reverted).not.toContain("oh-my-opencode@3.4.0")
|
||||
})
|
||||
|
||||
test("reverts to unpinned entry", () => {
|
||||
//#given
|
||||
const config = JSON.stringify({
|
||||
plugin: ["oh-my-opencode@3.4.0"],
|
||||
})
|
||||
fs.writeFileSync(configPath, config)
|
||||
|
||||
//#when
|
||||
const result = revertPinnedVersion(configPath, "3.4.0", "oh-my-opencode")
|
||||
|
||||
//#then
|
||||
expect(result).toBe(true)
|
||||
const reverted = fs.readFileSync(configPath, "utf-8")
|
||||
expect(reverted).toContain('"oh-my-opencode"')
|
||||
expect(reverted).not.toContain("oh-my-opencode@3.4.0")
|
||||
})
|
||||
|
||||
test("returns false when failed version not found", () => {
|
||||
//#given
|
||||
const config = JSON.stringify({
|
||||
plugin: ["oh-my-opencode@3.1.8"],
|
||||
})
|
||||
fs.writeFileSync(configPath, config)
|
||||
|
||||
//#when
|
||||
const result = revertPinnedVersion(configPath, "3.4.0", "oh-my-opencode@3.1.8")
|
||||
|
||||
//#then
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("update then revert roundtrip", () => {
|
||||
test("config returns to original state after update + revert", () => {
|
||||
//#given
|
||||
const originalConfig = JSON.stringify({
|
||||
plugin: ["oh-my-opencode@3.1.8"],
|
||||
})
|
||||
fs.writeFileSync(configPath, originalConfig)
|
||||
|
||||
//#when
|
||||
updatePinnedVersion(configPath, "oh-my-opencode@3.1.8", "3.4.0")
|
||||
revertPinnedVersion(configPath, "3.4.0", "oh-my-opencode@3.1.8")
|
||||
|
||||
//#then
|
||||
const finalConfig = fs.readFileSync(configPath, "utf-8")
|
||||
expect(finalConfig).toContain("oh-my-opencode@3.1.8")
|
||||
expect(finalConfig).not.toContain("oh-my-opencode@3.4.0")
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -2,10 +2,9 @@ import * as fs from "node:fs"
|
||||
import { log } from "../../../shared/logger"
|
||||
import { PACKAGE_NAME } from "../constants"
|
||||
|
||||
export function updatePinnedVersion(configPath: string, oldEntry: string, newVersion: string): boolean {
|
||||
function replacePluginEntry(configPath: string, oldEntry: string, newEntry: string): boolean {
|
||||
try {
|
||||
const content = fs.readFileSync(configPath, "utf-8")
|
||||
const newEntry = `${PACKAGE_NAME}@${newVersion}`
|
||||
|
||||
const pluginMatch = content.match(/"plugin"\s*:\s*\[/)
|
||||
if (!pluginMatch || pluginMatch.index === undefined) {
|
||||
@ -51,3 +50,13 @@ export function updatePinnedVersion(configPath: string, oldEntry: string, newVer
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function updatePinnedVersion(configPath: string, oldEntry: string, newVersion: string): boolean {
|
||||
const newEntry = `${PACKAGE_NAME}@${newVersion}`
|
||||
return replacePluginEntry(configPath, oldEntry, newEntry)
|
||||
}
|
||||
|
||||
export function revertPinnedVersion(configPath: string, failedVersion: string, originalEntry: string): boolean {
|
||||
const failedEntry = `${PACKAGE_NAME}@${failedVersion}`
|
||||
return replacePluginEntry(configPath, failedEntry, originalEntry)
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ import { log } from "../../../shared/logger"
|
||||
import { invalidatePackage } from "../cache"
|
||||
import { PACKAGE_NAME } from "../constants"
|
||||
import { extractChannel } from "../version-channel"
|
||||
import { findPluginEntry, getCachedVersion, getLatestVersion, updatePinnedVersion } from "../checker"
|
||||
import { findPluginEntry, getCachedVersion, getLatestVersion, updatePinnedVersion, revertPinnedVersion } from "../checker"
|
||||
import { showAutoUpdatedToast, showUpdateAvailableToast } from "./update-toasts"
|
||||
|
||||
async function runBunInstallSafe(): Promise<boolean> {
|
||||
@ -72,8 +72,14 @@ export async function runBackgroundUpdateCheck(
|
||||
if (installSuccess) {
|
||||
await showAutoUpdatedToast(ctx, currentVersion, latestVersion)
|
||||
log(`[auto-update-checker] Update installed: ${currentVersion} → ${latestVersion}`)
|
||||
} else {
|
||||
await showUpdateAvailableToast(ctx, latestVersion, getToastMessage)
|
||||
log("[auto-update-checker] bun install failed; update not installed (falling back to notification-only)")
|
||||
return
|
||||
}
|
||||
|
||||
if (pluginInfo.isPinned) {
|
||||
revertPinnedVersion(pluginInfo.configPath, latestVersion, pluginInfo.entry)
|
||||
log("[auto-update-checker] Config reverted due to install failure")
|
||||
}
|
||||
|
||||
await showUpdateAvailableToast(ctx, latestVersion, getToastMessage)
|
||||
log("[auto-update-checker] bun install failed; update not installed (falling back to notification-only)")
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user