From 3fd128832c9dd928e3b7c82faf1f0e8d22ee7ce2 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Tue, 12 May 2026 04:03:56 +0000 Subject: [PATCH] fix: add retry logic to copyDir/copyPaths for EBUSY errors on Windows The Windows CI bundle step fails with EBUSY when antivirus or indexing services hold brief locks on files during copyFileSync. Add a copyFileWithRetry helper (matching the existing rmDir retry pattern) that retries up to 5 times with exponential backoff for EBUSY, EPERM, and EACCES errors. --- packages/build/src/esbuild.ts | 40 +++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/packages/build/src/esbuild.ts b/packages/build/src/esbuild.ts index 952e823eec..c489801512 100644 --- a/packages/build/src/esbuild.ts +++ b/packages/build/src/esbuild.ts @@ -4,6 +4,42 @@ import { execSync } from "child_process" import { ViewsContainer, Views, Menus, Configuration, Keybindings, contributesSchema } from "./types.js" +/** + * Copy a single file with retry logic to handle transient Windows file-locking + * errors (EBUSY, EPERM, EACCES) that occur when antivirus or indexing services + * hold brief locks on files during CI builds. + */ +function copyFileWithRetry(src: string, dst: string, maxRetries: number = 5): void { + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + fs.copyFileSync(src, dst) + return + } catch (error) { + const isRetryable = + error instanceof Error && + "code" in error && + ((error as NodeJS.ErrnoException).code === "EBUSY" || + (error as NodeJS.ErrnoException).code === "EPERM" || + (error as NodeJS.ErrnoException).code === "EACCES") + + if (!isRetryable || attempt === maxRetries) { + throw error + } + + const baseDelay = process.platform === "win32" ? 200 : 100 + const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), 2000) + console.warn(`[copyFileWithRetry] Attempt ${attempt} failed for ${src}, retrying in ${delay}ms...`) + + // Synchronous sleep (same pattern as rmDir). + const start = Date.now() + + while (Date.now() - start < delay) { + /* Busy wait */ + } + } + } +} + function copyDir(srcDir: string, dstDir: string, count: number): number { const entries = fs.readdirSync(srcDir, { withFileTypes: true }) @@ -16,7 +52,7 @@ function copyDir(srcDir: string, dstDir: string, count: number): number { count = copyDir(srcPath, dstPath, count) } else { count = count + 1 - fs.copyFileSync(srcPath, dstPath) + copyFileWithRetry(srcPath, dstPath) } } @@ -98,7 +134,7 @@ export function copyPaths(copyPaths: [string, string, CopyPathOptions?][], srcDi const count = copyDir(path.join(srcDir, srcRelPath), path.join(dstDir, dstRelPath), 0) console.log(`[copyPaths] Copied ${count} files from ${srcRelPath} to ${dstRelPath}`) } else { - fs.copyFileSync(path.join(srcDir, srcRelPath), path.join(dstDir, dstRelPath)) + copyFileWithRetry(path.join(srcDir, srcRelPath), path.join(dstDir, dstRelPath)) console.log(`[copyPaths] Copied ${srcRelPath} to ${dstRelPath}`) } } catch (error) {