Skip to content

Commit 0aa70dd

Browse files
committed
release/index.js: Download binary to temp file, after smoke test, move to final location
1 parent 896e0a8 commit 0aa70dd

File tree

1 file changed

+107
-38
lines changed

1 file changed

+107
-38
lines changed

cli/release/index.js

Lines changed: 107 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ function createConfig(packageName) {
2323
binaryName,
2424
binaryPath: path.join(configDir, binaryName),
2525
metadataPath: path.join(configDir, 'codebuff-metadata.json'),
26+
tempDownloadDir: path.join(configDir, '.download-temp'),
2627
userAgent: `${packageName}-cli`,
2728
requestTimeout: 20000,
2829
}
@@ -127,6 +128,51 @@ function getCurrentVersion() {
127128
}
128129
}
129130

131+
function runSmokeTest(binaryPath) {
132+
return new Promise((resolve) => {
133+
if (!fs.existsSync(binaryPath)) {
134+
resolve(false)
135+
return
136+
}
137+
138+
const child = spawn(binaryPath, ['--version'], {
139+
cwd: os.homedir(),
140+
stdio: 'pipe',
141+
})
142+
143+
let output = ''
144+
145+
child.stdout.on('data', (data) => {
146+
output += data.toString()
147+
})
148+
149+
const timeout = setTimeout(() => {
150+
child.kill('SIGTERM')
151+
setTimeout(() => {
152+
if (!child.killed) {
153+
child.kill('SIGKILL')
154+
}
155+
}, 1000)
156+
resolve(false)
157+
}, 5000)
158+
159+
child.on('exit', (code) => {
160+
clearTimeout(timeout)
161+
// Check that it exits successfully and outputs something that looks like a version
162+
if (code === 0 && output.trim().match(/^\d+(\.\d+)*$/)) {
163+
resolve(true)
164+
} else {
165+
resolve(false)
166+
}
167+
})
168+
169+
child.on('error', () => {
170+
clearTimeout(timeout)
171+
resolve(false)
172+
})
173+
})
174+
}
175+
130176
function compareVersions(v1, v2) {
131177
if (!v1 || !v2) return 0
132178

@@ -217,33 +263,21 @@ async function downloadBinary(version) {
217263
process.env.NEXT_PUBLIC_CODEBUFF_APP_URL || 'https://codebuff.com'
218264
}/api/releases/download/${version}/${fileName}`
219265

266+
// Ensure config directory exists
220267
fs.mkdirSync(CONFIG.configDir, { recursive: true })
221268

222-
if (fs.existsSync(CONFIG.binaryPath)) {
223-
try {
224-
fs.unlinkSync(CONFIG.binaryPath)
225-
} catch (err) {
226-
// Fallback: try renaming the locked/undeletable binary
227-
const backupPath = CONFIG.binaryPath + `.old.${Date.now()}`
228-
229-
try {
230-
fs.renameSync(CONFIG.binaryPath, backupPath)
231-
} catch (renameErr) {
232-
// If we can't unlink OR rename, we can't safely proceed
233-
throw new Error(
234-
`Failed to replace existing binary. ` +
235-
`unlink error: ${err.code || err.message}, ` +
236-
`rename error: ${renameErr.code || renameErr.message}`,
237-
)
238-
}
239-
}
269+
// Clean up any previous temp download directory
270+
if (fs.existsSync(CONFIG.tempDownloadDir)) {
271+
fs.rmSync(CONFIG.tempDownloadDir, { recursive: true })
240272
}
273+
fs.mkdirSync(CONFIG.tempDownloadDir, { recursive: true })
241274

242275
term.write('Downloading...')
243276

244277
const res = await httpGet(downloadUrl)
245278

246279
if (res.statusCode !== 200) {
280+
fs.rmSync(CONFIG.tempDownloadDir, { recursive: true })
247281
throw new Error(`Download failed: HTTP ${res.statusCode}`)
248282
}
249283

@@ -269,36 +303,71 @@ async function downloadBinary(version) {
269303
}
270304
})
271305

306+
// Extract to temp directory
272307
await new Promise((resolve, reject) => {
273308
res
274309
.pipe(zlib.createGunzip())
275-
.pipe(tar.x({ cwd: CONFIG.configDir }))
310+
.pipe(tar.x({ cwd: CONFIG.tempDownloadDir }))
276311
.on('finish', resolve)
277312
.on('error', reject)
278313
})
279314

280-
try {
281-
const files = fs.readdirSync(CONFIG.configDir)
282-
const extractedPath = path.join(CONFIG.configDir, CONFIG.binaryName)
315+
const tempBinaryPath = path.join(CONFIG.tempDownloadDir, CONFIG.binaryName)
316+
317+
// Verify the binary was extracted
318+
if (!fs.existsSync(tempBinaryPath)) {
319+
const files = fs.readdirSync(CONFIG.tempDownloadDir)
320+
fs.rmSync(CONFIG.tempDownloadDir, { recursive: true })
321+
throw new Error(
322+
`Binary not found after extraction. Expected: ${CONFIG.binaryName}, Available files: ${files.join(', ')}`,
323+
)
324+
}
325+
326+
// Set executable permissions
327+
if (process.platform !== 'win32') {
328+
fs.chmodSync(tempBinaryPath, 0o755)
329+
}
330+
331+
// Run smoke test on the downloaded binary
332+
term.write('Verifying download...')
333+
const smokeTestPassed = await runSmokeTest(tempBinaryPath)
334+
335+
if (!smokeTestPassed) {
336+
fs.rmSync(CONFIG.tempDownloadDir, { recursive: true })
337+
throw new Error('Downloaded binary failed smoke test (--version check)')
338+
}
283339

284-
if (fs.existsSync(extractedPath)) {
285-
if (process.platform !== 'win32') {
286-
fs.chmodSync(extractedPath, 0o755)
340+
// Smoke test passed - move binary to final location
341+
try {
342+
if (fs.existsSync(CONFIG.binaryPath)) {
343+
try {
344+
fs.unlinkSync(CONFIG.binaryPath)
345+
} catch (err) {
346+
// Fallback: try renaming the locked/undeletable binary (Windows)
347+
const backupPath = CONFIG.binaryPath + `.old.${Date.now()}`
348+
try {
349+
fs.renameSync(CONFIG.binaryPath, backupPath)
350+
} catch (renameErr) {
351+
throw new Error(
352+
`Failed to replace existing binary. ` +
353+
`unlink error: ${err.code || err.message}, ` +
354+
`rename error: ${renameErr.code || renameErr.message}`,
355+
)
356+
}
287357
}
288-
// Save version metadata for fast version checking
289-
fs.writeFileSync(
290-
CONFIG.metadataPath,
291-
JSON.stringify({ version }, null, 2),
292-
)
293-
} else {
294-
throw new Error(
295-
`Binary not found after extraction. Expected: ${extractedPath}, Available files: ${files.join(', ')}`,
296-
)
297358
}
298-
} catch (error) {
299-
term.clearLine()
300-
console.error(`Extraction failed: ${error.message}`)
301-
process.exit(1)
359+
fs.renameSync(tempBinaryPath, CONFIG.binaryPath)
360+
361+
// Save version metadata for fast version checking
362+
fs.writeFileSync(
363+
CONFIG.metadataPath,
364+
JSON.stringify({ version }, null, 2),
365+
)
366+
} finally {
367+
// Clean up temp directory even if rename fails
368+
if (fs.existsSync(CONFIG.tempDownloadDir)) {
369+
fs.rmSync(CONFIG.tempDownloadDir, { recursive: true })
370+
}
302371
}
303372

304373
term.clearLine()

0 commit comments

Comments
 (0)