mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
fix(cli): retry transient npm publish failures
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
export namespace NpmPublish {
|
||||
const attempts = 3
|
||||
const base = 10_000
|
||||
const jitter = 5_000
|
||||
|
||||
export async function retry(input: {
|
||||
name: string
|
||||
version: string
|
||||
run: () => Promise<unknown>
|
||||
exists: () => Promise<boolean>
|
||||
sleep?: (ms: number) => Promise<void>
|
||||
}) {
|
||||
for (let attempt = 1; attempt <= attempts; attempt++) {
|
||||
try {
|
||||
await input.run()
|
||||
return
|
||||
} catch (err) {
|
||||
if (await input.exists()) {
|
||||
console.log(`published ${input.name}@${input.version} despite a failed npm publish command`)
|
||||
return
|
||||
}
|
||||
if (attempt === attempts) throw err
|
||||
|
||||
const delay = attempt * base + Math.floor(Math.random() * jitter)
|
||||
console.warn(
|
||||
`npm publish ${input.name}@${input.version} failed (attempt ${attempt}/${attempts}), retrying in ${delay / 1000}s`,
|
||||
)
|
||||
await (input.sleep ?? Bun.sleep)(delay)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { $ } from "bun"
|
||||
import pkg from "../package.json"
|
||||
import { Script } from "@opencode-ai/script"
|
||||
import { fileURLToPath } from "url"
|
||||
import { NpmPublish } from "./kilocode/npm-publish" // kilocode_change
|
||||
|
||||
const dir = fileURLToPath(new URL("..", import.meta.url))
|
||||
process.chdir(dir)
|
||||
@@ -20,7 +21,14 @@ async function publish(dir: string, name: string, version: string) {
|
||||
return
|
||||
}
|
||||
await $`bun pm pack`.cwd(dir)
|
||||
await $`npm publish *.tgz --access public --tag ${Script.channel} --provenance`.cwd(dir) // kilocode_change
|
||||
// kilocode_change start
|
||||
await NpmPublish.retry({
|
||||
name,
|
||||
version,
|
||||
run: () => $`npm publish *.tgz --access public --tag ${Script.channel} --provenance`.cwd(dir),
|
||||
exists: () => published(name, version),
|
||||
})
|
||||
// kilocode_change end
|
||||
}
|
||||
|
||||
const binaries: Record<string, string> = {}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { NpmPublish } from "../../script/kilocode/npm-publish"
|
||||
|
||||
describe("npm publish retry", () => {
|
||||
test("returns after the first successful attempt", async () => {
|
||||
const calls = { run: 0, exists: 0, sleep: 0 }
|
||||
|
||||
await NpmPublish.retry({
|
||||
name: "@kilocode/test",
|
||||
version: "1.0.0",
|
||||
run: async () => {
|
||||
calls.run++
|
||||
},
|
||||
exists: async () => {
|
||||
calls.exists++
|
||||
return false
|
||||
},
|
||||
sleep: async () => {
|
||||
calls.sleep++
|
||||
},
|
||||
})
|
||||
|
||||
expect(calls).toEqual({ run: 1, exists: 0, sleep: 0 })
|
||||
})
|
||||
|
||||
test("accepts a version that landed after a failed command", async () => {
|
||||
const calls = { run: 0, exists: 0, sleep: 0 }
|
||||
const err = new Error("connection closed")
|
||||
|
||||
await NpmPublish.retry({
|
||||
name: "@kilocode/test",
|
||||
version: "1.0.0",
|
||||
run: async () => {
|
||||
calls.run++
|
||||
throw err
|
||||
},
|
||||
exists: async () => {
|
||||
calls.exists++
|
||||
return true
|
||||
},
|
||||
sleep: async () => {
|
||||
calls.sleep++
|
||||
},
|
||||
})
|
||||
|
||||
expect(calls).toEqual({ run: 1, exists: 1, sleep: 0 })
|
||||
})
|
||||
|
||||
test("retries an unpublished version after a delay", async () => {
|
||||
const calls = { run: 0, exists: 0 }
|
||||
const delays: number[] = []
|
||||
const err = new Error("registry unavailable")
|
||||
|
||||
await NpmPublish.retry({
|
||||
name: "@kilocode/test",
|
||||
version: "1.0.0",
|
||||
run: async () => {
|
||||
calls.run++
|
||||
if (calls.run === 1) throw err
|
||||
},
|
||||
exists: async () => {
|
||||
calls.exists++
|
||||
return false
|
||||
},
|
||||
sleep: async (ms) => {
|
||||
delays.push(ms)
|
||||
},
|
||||
})
|
||||
|
||||
expect(calls).toEqual({ run: 2, exists: 1 })
|
||||
expect(delays).toHaveLength(1)
|
||||
expect(delays[0]).toBeGreaterThanOrEqual(10_000)
|
||||
expect(delays[0]).toBeLessThan(15_000)
|
||||
})
|
||||
|
||||
test("accepts a version that becomes visible after a retry", async () => {
|
||||
const calls = { run: 0, exists: 0 }
|
||||
const delays: number[] = []
|
||||
const err = new Error("registry response lost")
|
||||
|
||||
await NpmPublish.retry({
|
||||
name: "@kilocode/test",
|
||||
version: "1.0.0",
|
||||
run: async () => {
|
||||
calls.run++
|
||||
throw err
|
||||
},
|
||||
exists: async () => {
|
||||
calls.exists++
|
||||
return calls.exists === 2
|
||||
},
|
||||
sleep: async (ms) => {
|
||||
delays.push(ms)
|
||||
},
|
||||
})
|
||||
|
||||
expect(calls).toEqual({ run: 2, exists: 2 })
|
||||
expect(delays).toHaveLength(1)
|
||||
})
|
||||
|
||||
test("preserves the error after all attempts fail", async () => {
|
||||
const calls = { run: 0, exists: 0 }
|
||||
const delays: number[] = []
|
||||
const err = new Error("permission denied")
|
||||
|
||||
const failure = await NpmPublish.retry({
|
||||
name: "@kilocode/test",
|
||||
version: "1.0.0",
|
||||
run: async () => {
|
||||
calls.run++
|
||||
throw err
|
||||
},
|
||||
exists: async () => {
|
||||
calls.exists++
|
||||
return false
|
||||
},
|
||||
sleep: async (ms) => {
|
||||
delays.push(ms)
|
||||
},
|
||||
}).then(
|
||||
() => undefined,
|
||||
(error) => error,
|
||||
)
|
||||
|
||||
expect(failure).toBe(err)
|
||||
expect(calls).toEqual({ run: 3, exists: 3 })
|
||||
expect(delays).toHaveLength(2)
|
||||
expect(delays[0]).toBeGreaterThanOrEqual(10_000)
|
||||
expect(delays[0]).toBeLessThan(15_000)
|
||||
expect(delays[1]).toBeGreaterThanOrEqual(20_000)
|
||||
expect(delays[1]).toBeLessThan(25_000)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user