From 3ad13eb7029abc7f738952a8433c850b1aff6dfd Mon Sep 17 00:00:00 2001 From: Faustze Date: Thu, 23 Jul 2026 15:58:37 +0300 Subject: [PATCH] fix: retry plugin install/build on transient npm failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A scheduled deploy run failed because npm silently resolved only the production dependency subtree for two plugins (missing tsup), while 14 prior runs of the same workflow succeeded — a one-off npm registry flake, not a code bug. Wipe node_modules and retry install+build up to 3 times so a transient hiccup no longer fails the whole deploy. --- quartz/plugins/loader/gitLoader.ts | 72 ++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 24 deletions(-) diff --git a/quartz/plugins/loader/gitLoader.ts b/quartz/plugins/loader/gitLoader.ts index 64618b12c7e8b..5e25ceb7f8a17 100644 --- a/quartz/plugins/loader/gitLoader.ts +++ b/quartz/plugins/loader/gitLoader.ts @@ -357,6 +357,8 @@ function linkPeerDependencies(pluginDir: string): void { } } +const INSTALL_BUILD_ATTEMPTS = 3 + function buildInstalledPlugin(pluginDir: string, name: string, verbose?: boolean): void { if (hasPrebuiltDist(pluginDir)) { if (verbose) { @@ -366,41 +368,63 @@ function buildInstalledPlugin(pluginDir: string, name: string, verbose?: boolean return } - try { - const shouldBuild = needsBuild(pluginDir) + const shouldBuild = needsBuild(pluginDir) + let lastError: unknown - if (verbose) { - console.log(styleText("cyan", `→`), `${name}: installing dependencies...`) - } - execSync("npm install --ignore-scripts", { - cwd: pluginDir, - stdio: verbose ? "inherit" : "pipe", - timeout: 120_000, - }) + for (let attempt = 1; attempt <= INSTALL_BUILD_ATTEMPTS; attempt++) { + try { + // A registry hiccup can make npm silently resolve only the production + // subtree (missing devDependencies like the build tool itself), so wipe + // any partial install before retrying rather than layering on top of it. + if (attempt > 1) { + if (verbose) { + console.log( + styleText("yellow", `⟳`), + `${name}: retrying install (attempt ${attempt}/${INSTALL_BUILD_ATTEMPTS})...`, + ) + } + fs.rmSync(path.join(pluginDir, "node_modules"), { recursive: true, force: true }) + } - if (shouldBuild) { if (verbose) { - console.log(styleText("cyan", `→`), `${name}: building...`) + console.log(styleText("cyan", `→`), `${name}: installing dependencies...`) } - execSync("npm run build", { + execSync("npm install --ignore-scripts", { cwd: pluginDir, stdio: verbose ? "inherit" : "pipe", timeout: 120_000, }) - } - execSync("npm prune --omit=dev", { - cwd: pluginDir, - stdio: verbose ? "inherit" : "pipe", - timeout: 60_000, - }) + if (shouldBuild) { + if (verbose) { + console.log(styleText("cyan", `→`), `${name}: building...`) + } + execSync("npm run build", { + cwd: pluginDir, + stdio: verbose ? "inherit" : "pipe", + timeout: 120_000, + }) + } - linkPeerDependencies(pluginDir) - } catch (error) { - const message = error instanceof Error ? error.message : String(error) - console.error(styleText("red", `✗`), `${name}: post-install build failed: ${message}`) - throw new Error(`Failed to build plugin ${name}: ${message}`) + execSync("npm prune --omit=dev", { + cwd: pluginDir, + stdio: verbose ? "inherit" : "pipe", + timeout: 60_000, + }) + + linkPeerDependencies(pluginDir) + return + } catch (error) { + lastError = error + } } + + const message = lastError instanceof Error ? lastError.message : String(lastError) + console.error( + styleText("red", `✗`), + `${name}: post-install build failed after ${INSTALL_BUILD_ATTEMPTS} attempts: ${message}`, + ) + throw new Error(`Failed to build plugin ${name}: ${message}`) } interface PluginInstallResult {