Skip to content
SlashClone
Made by@shipcheck

Deploy doctor

@shipcheck/deploy-doctor

Paste a failed Vercel or Netlify build log and get the most likely cause and a fix you can apply. Recognizes about twenty common failures: letter-case import errors, missing packages, missing env vars, prerender errors, window is not defined, lockfile drift, peer dependency conflicts, wrong Node version, out of memory, wrong output or root folder, oversized functions, and more.

SkillDevOpsv1.0.0 (latest)Tested
Activity
New: be the first to try it
  1. 1#!/usr/bin/env node
  2. 2// Triage a failed Vercel or Netlify build log.
  3. 3// Detects platform, framework, package manager and Node version, then matches known
  4. 4// failure signatures and ranks them by where they appear (the first real error is
  5. 5// usually the cause; later errors are often knock-on effects).
  6. 6// Node built-ins only. Reads a file or stdin; never calls any host API.
  7. 7//
  8. 8// Usage: node scripts/triage-log.mjs [data/build-log.txt | -] [--json out/deploy-triage.json] [--md out/deploy-triage.md]
  9. 9
  10. 10import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
  11. 11import { dirname } from "node:path";
  12. 13function parseArgs(argv) {
  13. 14 const args = { input: "data/build-log.txt", json: null, md: null };
  14. 15 const rest = [];
  15. 16 for (let i = 0; i < argv.length; i++) {
  16. 17 const a = argv[i];
  17. 18 if (a === "--json") args.json = argv[++i];
  18. 19 else if (a === "--md") args.md = argv[++i];
  19. 20 else if (a === "-h" || a === "--help") {
  20. 21 console.log("Usage: node scripts/triage-log.mjs [log-file | -] [--json file] [--md file]");
  21. 22 process.exit(0);
  22. 23 } else rest.push(a);
  23. 24 }
  24. 25 if (rest[0]) args.input = rest[0];
  25. 26 return args;
  26. 29// Each signature: id, title, patterns (first match wins), cause, fix (steps). `{1}` in text is the
  27. 30// first capture; `[? ... ?]` is kept only when there is a capture.
  28. 31const SIGNATURES = [
  29. 32 {
  30. 33 id: "missing-module-local",
  31. 34 title: "Import path not found (often letter case)",
  32. 35 patterns: [/Module not found: (?:Error: )?Can't resolve '((?:\.{1,2}\/|@\/|~\/|src\/)[^']+)'/, /Failed to resolve import "((?:\.{1,2}\/|@\/|~\/)[^"]+)"/, /Cannot find module '((?:\.{1,2}\/|@\/)[^']+)'/],
  33. 36 cause: "The file '{1}' cannot be found on the build server. Builds run on Linux, where 'Button.tsx' and 'button.tsx' are different files, while your Mac treats them as the same.",
  34. 37 fix: [
  35. 38 "Check the exact spelling and letter case of '{1}' against the file on disk.",
  36. 39 "If only the case differs, rename through git so it sees the change: git mv src/components/button.tsx src/components/Button.tsx (via a temp name if needed).",
  37. 40 "If the file is in .gitignore or was never committed, commit it.",
  38. 41 ],
  39. 42 },
  40. 43 {
  41. 44 id: "missing-module-package",
  42. 45 title: "Package not installed",
  43. 46 patterns: [
  44. 47 /Module not found: (?:Error: )?Can't resolve '((?!\.|@\/|~\/|src\/)[^']+)'/,
  45. 48 /Cannot find module '((?!\.|@\/)[^']+)'/,
  46. 49 /Failed to resolve import "((?!\.|@\/|~\/)[^"]+)"/,
  47. 50 /Cannot find package '([^']+)'/,
  48. 51 ],
  49. 52 cause: "'{1}' is imported but not installed on the build server. Usually it is missing from package.json, or only installed globally on your machine.",
  50. 53 fix: [
  51. 54 "Add it to dependencies (not devDependencies if it is used at runtime) and commit package.json and the lockfile.",
  52. 55 "If it is a Node built-in like fs or path, that code is being bundled for the browser or edge. Move it to server-only code.",
  53. 56 ],
  54. 57 },
  55. 58 {
  56. 59 id: "edge-node-module",
  57. 60 title: "Node API used in the edge runtime",
  58. 61 patterns: [/edge runtime does not support Node\.js '([^']+)' module/i, /A Node\.js (?:API|module) is used \(([^)]+)\) which is not supported in the Edge Runtime/],
  59. 62 cause: "Code running on the edge (middleware or a route with runtime = 'edge') uses the Node module '{1}', which the edge does not have.",
  60. 63 fix: ["Remove export const runtime = 'edge' from that route, or swap the library for an edge-compatible one.", "Keep middleware small: no database drivers or Node SDKs in middleware."],
  61. 64 },
  62. 65 {
  63. 66 id: "missing-env",
  64. 67 title: "Environment variable missing at build time",
  65. 68 patterns: [
  66. 69 /Environment variable not found: ([A-Z0-9_]+)/,
  67. 70 /Missing (?:required )?environment variable[s]?:? ?["'`]?([A-Z0-9_]+)?/i,
  68. 71 /(supabaseUrl|supabaseKey) is required/,
  69. 72 /The ([A-Z0-9_]+) environment variable is missing/,
  70. 73 /Neither apiKey nor config\.authenticator provided/,
  71. 74 /Invalid environment variables/,
  72. 75 /env(?:ironment)? variable ["'`]?([A-Z0-9_]{3,})["'`]? (?:is )?(?:not set|undefined|missing)/i,
  73. 76 ],
  74. 77 cause: "A required environment variable is not set for this build[? (the log names '{1}')?]. Values in your local .env file are not uploaded to the host.",
  75. 78 fix: [
  76. 79 "Add the variable in the host's project settings, under Environment Variables, for the environment that failed (Production or Preview).",
  77. 80 "Variables with NEXT_PUBLIC_ or VITE_ prefixes are baked in at build time, so redeploy after adding them.",
  78. 81 "If the code reads it at import time (top of a file), it also runs during build. Move the read inside the function that needs it.",
  79. 82 ],
  80. 83 },
  81. 84 {
  82. 85 id: "prerender-error",
  83. 86 title: "A page failed while being prerendered",
  84. 87 patterns: [/Error occurred prerendering page "([^"]+)"/],
  85. 88 cause: "Next.js renders '{1}' at build time and it threw. Usually a fetch to your own API (not running during build), a missing env var, or data that is undefined at build time.",
  86. 89 fix: [
  87. 90 "Read the error just above this line in the log; it names the real cause.",
  88. 91 "Do not fetch your own /api routes from server components. Call the function directly.",
  89. 92 "If the page needs request-time data, mark it dynamic: export const dynamic = 'force-dynamic'.",
  90. 93 ],
  91. 94 },
  92. 95 {
  93. 96 id: "browser-global-on-server",
  94. 97 title: "Browser-only code ran on the server",
  95. 98 patterns: [/ReferenceError: (window|document|localStorage|navigator|self) is not defined/],
  96. 99 cause: "'{1}' only exists in the browser, but this code ran during server rendering or the build.",
  97. 100 fix: ["Add 'use client' to the component and touch '{1}' only inside useEffect.", "For a library that needs the browser, load it with next/dynamic and ssr: false."],
  98. 101 },
  99. 102 {
  100. 103 id: "use-search-params-suspense",
  101. 104 title: "useSearchParams needs a Suspense boundary",
  102. 105 patterns: [/useSearchParams\(\) should be wrapped in a suspense boundary(?: at page "([^"]+)")?/i],
  103. 106 cause: "A client component reads search params on a page that Next.js tries to render statically.",
  104. 107 fix: ["Wrap the component that calls useSearchParams in <Suspense fallback={null}>.", "Or read searchParams in the page (server) and pass them down as props."],
  105. 108 },
  106. 109 {
  107. 110 id: "dynamic-server-usage",
  108. 111 title: "Static page uses request data",
  109. 112 patterns: [/Dynamic server usage: ?([^\n]*)/, /couldn't be rendered statically because it used ([^\n]+)/],
  110. 113 cause: "A page Next.js wanted to build statically reads cookies, headers or search params ({1}).",
  111. 114 fix: ["Add export const dynamic = 'force-dynamic' to that page or route, or move the request-time read out of it."],
  112. 115 },
  113. 116 {
  114. 117 id: "type-error",
  115. 118 title: "TypeScript error fails the build",
  116. 119 patterns: [/Type error: ([^\n]+)/, /error TS\d+: ([^\n]+)/],
  117. 120 cause: "The production build type-checks your code and found: {1}",
  118. 121 fix: [
  119. 122 "Run the build locally (npm run build) to see the same error with the file and line.",
  120. 123 "Fix the type rather than setting typescript.ignoreBuildErrors; that flag hides real bugs.",
  121. 124 ],
  122. 125 },
  123. 126 {
  124. 127 id: "lint-error",
  125. 128 title: "Lint errors fail the build",
  126. 129 patterns: [/(?:ESLint|eslint).{0,40}(?:error|Error)[^\n]*/, /Error: +[^\n]*\s(react\/[a-z-]+|@typescript-eslint\/[a-z-]+|react-hooks\/[a-z-]+)/],
  127. 130 cause: "The build runs the linter and treats errors as failures.",
  128. 131 fix: ["Run npm run lint locally and fix the listed lines.", "Common one: react/no-unescaped-entities. Write &apos; instead of a raw apostrophe in JSX text."],
  129. 132 },
  130. 133 {
  131. 134 id: "lockfile-out-of-sync",
  132. 135 title: "Lockfile does not match package.json",
  133. 136 patterns: [
  134. 137 /`npm ci` can only install packages when your package\.json and package-lock\.json[^\n]* are in sync/,
  135. 138 /ERR_PNPM_OUTDATED_LOCKFILE/,
  136. 139 /Your lockfile needs to be updated, but yarn was run with `--frozen-lockfile`/,
  137. 140 /lockfile had changes, but lockfile is frozen/,
  138. 141 /ERR_PNPM_LOCKFILE_CONFIG_MISMATCH/,
  139. 142 ],
  140. 143 cause: "package.json changed but the lockfile was not updated and committed, or two lockfiles (npm and pnpm, say) disagree.",
  141. 144 fix: [
  142. 145 "Run your package manager's install locally (npm install, pnpm install, or yarn) and commit the updated lockfile.",
  143. 146 "Keep exactly one lockfile. Delete the others so the host picks the right package manager.",
  144. 147 ],
  145. 148 },
  146. 149 {
  147. 150 id: "peer-deps",
  148. 151 title: "npm peer dependency conflict",
  149. 152 patterns: [/ERESOLVE (?:unable to resolve dependency tree|could not resolve)/],
  150. 153 cause: "Two packages want incompatible versions of a shared dependency (often React).",
  151. 154 fix: [
  152. 155 "Read the 'Could not resolve dependency' lines to see which package needs which version, and upgrade that package.",
  153. 156 "As a stopgap, add legacy-peer-deps=true to an .npmrc in the repo. Treat it as a temporary fix.",
  154. 157 ],
  155. 158 },
  156. 159 {
  157. 160 id: "node-version",
  158. 161 title: "Wrong Node.js version",
  159. 162 patterns: [/The engine "node" is incompatible with this module\. Expected version "([^"]+)"/, /requires (?:a )?Node(?:\.js)? (?:version )?([>=^~\d.x ]+)/i, /Node\.js version ([\d.]+) (?:has reached End-of-Life|is not supported|is deprecated)/i, /You are using Node\.js ([\d.]+)/],
  160. 163 cause: "The build server uses a Node version that a package or the framework does not support ({1}).",
  161. 164 fix: [
  162. 165 "Vercel: Project Settings, General, Node.js Version. Netlify: set NODE_VERSION in environment variables or add an .nvmrc file.",
  163. 166 "Also set \"engines\": { \"node\": \"22.x\" } in package.json so local and host agree.",
  164. 167 ],
  165. 168 },
  166. 169 {
  167. 170 id: "out-of-memory",
  168. 171 title: "Build ran out of memory",
  169. 172 patterns: [/JavaScript heap out of memory/, /Reached heap limit/, /exited with (?:code )?137/, /\bSIGKILL\b/],
  170. 173 cause: "The build used more memory than the build machine has.",
  171. 174 fix: [
  172. 175 "Set NODE_OPTIONS=--max-old-space-size=4096 in the build environment variables.",
  173. 176 "Look for huge imports (whole icon packs, big JSON files) and turn off production source maps if you do not use them.",
  174. 177 ],
  175. 178 },
  176. 179 {
  177. 180 id: "missing-build-script",
  178. 181 title: "No build script",
  179. 182 patterns: [/Missing script: "?build"?/i, /missing script: build/i],
  180. 183 cause: "The host runs the build command but package.json has no matching script.",
  181. 184 fix: ["Add \"build\" to scripts in package.json, or change the build command in the host settings to the one you use locally."],
  182. 185 },
  183. 186 {
  184. 187 id: "command-not-found",
  185. 188 title: "Build command not found",
  186. 189 patterns: [/(?:sh|bash): (?:line \d+: )?([\w.-]+): (?:command )?not found/, /exit(?:ed with)? code:? 127/i],
  187. 190 cause: "The build calls a command[? ('{1}')?] that is not installed on the build server.",
  188. 191 fix: ["If it is a package CLI, add the package to devDependencies so it lands in node_modules/.bin.", "Check the build command in the host settings for typos."],
  189. 192 },
  190. 193 {
  191. 194 id: "output-dir",
  192. 195 title: "Output folder not found",
  193. 196 patterns: [/No Output Directory named "([^"]+)" found/, /Deploy directory '([^']+)' does not exist/, /publish directory[^\n]*'([^']+)'[^\n]*does not exist/i],
  194. 197 cause: "The build finished but the folder the host deploys ('{1}') does not exist.",
  195. 198 fix: [
  196. 199 "Set the output or publish directory to what your framework writes: Next.js on Vercel needs none (framework preset), Vite uses dist, Create React App uses build, Astro uses dist.",
  197. 200 "On Netlify with Next.js, use the Next.js runtime rather than a publish folder.",
  198. 201 ],
  199. 202 },
  200. 203 {
  201. 204 id: "root-directory",
  202. 205 title: "Wrong root or base directory",
  203. 206 patterns: [/The specified Root Directory "([^"]+)" does not exist/, /Base directory does not exist: ?([^\n]*)/, /ENOENT: no such file or directory, open '([^']*package\.json)'/, /Could not read package\.json/],
  204. 207 cause: "The host looks for your app in the wrong folder ({1}). Common in monorepos or when the app lives in a subfolder.",
  205. 208 fix: ["Set Root Directory (Vercel) or Base directory (Netlify) to the folder that holds package.json."],
  206. 209 },
  207. 210 {
  208. 211 id: "function-too-large",
  209. 212 title: "Serverless function too large",
  210. 213 patterns: [/exceeds? the (?:maximum|unzipped maximum) size/i, /Serverless Function[^\n]*exceeded[^\n]*size/i, /function[^\n]*is too large/i],
  211. 214 cause: "A server function bundles too much, often a big dependency (headless browser, ML library) or files pulled in by a wide import.",
  212. 215 fix: ["Find the heavy dependency in the log and load it only where needed, or move that job to a separate service.", "Exclude large folders from the function trace (Next.js: outputFileTracingExcludes)."],
  213. 216 },
  214. 217 {
  215. 218 id: "prisma-client",
  216. 219 title: "Prisma client not generated",
  217. 220 patterns: [/@prisma\/client did not initialize yet/, /Prisma Client could not locate the Query Engine/, /Prisma has detected that this project was built on Vercel/],
  218. 221 cause: "The build uses a cached node_modules, so prisma generate never ran.",
  219. 222 fix: ["Add \"postinstall\": \"prisma generate\" to package.json scripts, or put prisma generate before the build command."],
  220. 223 },
  221. 224 {
  222. 225 id: "private-package",
  223. 226 title: "Private package or registry auth failed",
  224. 227 patterns: [/npm ERR! code E401/, /\b401 Unauthorized\b[^\n]*registry/i, /npm ERR! 404 Not Found - GET (\S+)/],
  225. 228 cause: "The build cannot download a package[? ({1})?]: it is private, misspelled, or needs a registry token.",
  226. 229 fix: ["Check the package name. For private packages, add the registry token as an env var on the host and reference it from .npmrc, never paste the token itself into .npmrc."],
  227. 230 },
  228. 231];
  229. 233const GENERIC_ERROR = /\b(?:Error|ERROR|error|Failed|FAILED|failed)\b/;
  230. 234const NOISE = /(?:npm WARN|warning|deprecated|Retrying|Cloning|Restored build cache|Downloading|Installing)/i;
  231. 236function detect(log) {
  232. 237 const platform = /Netlify Build|netlify\.toml|Netlify CLI|@netlify\//i.test(log)
  233. 238 ? "Netlify"
  234. 239 : /Vercel CLI|Running build in|vercel\.json|Build Completed in \/vercel|Deploying outputs/i.test(log)
  235. 240 ? "Vercel"
  236. 241 : "unknown";
  237. 242 const framework =
  238. 243 [
  239. 244 ["Next.js", /▲ Next\.js|next build|Next\.js \d/],
  240. 245 ["Vite", /vite v\d|vite build/],
  241. 246 ["Astro", /astro build|\bastro v?\d/i],
  242. 247 ["SvelteKit", /@sveltejs\/kit|svelte-kit/],
  243. 248 ["Nuxt", /nuxi build|Nuxt \d/],
  244. 249 ["Remix", /remix build|@remix-run/],
  245. 250 ["Create React App", /react-scripts build/],
  246. 251 ].find(([, re]) => re.test(log))?.[0] ?? "unknown";
  247. 252 const pm =
  248. 253 /pnpm install|ERR_PNPM|pnpm-lock/.test(log) ? "pnpm" : /yarn install|yarn\.lock|\byarn run\b/.test(log) ? "yarn" : /bun install|bun\.lockb?/.test(log) ? "bun" : /npm (?:ci|install|run)|npm ERR!|package-lock/.test(log) ? "npm" : "unknown";
  249. 254 const node = /(?:Node(?:\.js)? (?:version )?|node v)(\d+\.\d+(?:\.\d+)?)/i.exec(log)?.[1] ?? null;
  250. 255 return { platform, framework, package_manager: pm, node };
  251. 258function fill(text, capture) {
  252. 259 return text
  253. 260 .replace(/\[\?([^?]*)\?\]/g, (_, inner) => (capture ? inner.replace("{1}", capture) : ""))
  254. 261 .replace(/\{1\}/g, capture ?? "?");
  255. 264// Mask long token-like strings so pasted logs do not carry secrets into reports.
  256. 265function mask(line) {
  257. 266 return line.replace(/[A-Za-z0-9_\-+=]{32,}/g, (m) => `${m.slice(0, 4)}...[masked ${m.length}]`);
  258. 269export function triage(log) {
  259. 270 const lines = log.split(/\r?\n/);
  260. 271 const matches = [];
  261. 272 for (const sig of SIGNATURES) {
  262. 273 for (let i = 0; i < lines.length; i++) {
  263. 274 const m = sig.patterns.map((p) => p.exec(lines[i])).find(Boolean);
  264. 275 if (!m) continue;
  265. 276 const capture = m[1]?.trim() || null;
  266. 277 matches.push({
  267. 278 id: sig.id,
  268. 279 title: sig.title,
  269. 280 line: i + 1,
  270. 281 excerpt: mask(lines[i].trim()).slice(0, 300),
  271. 282 cause: fill(sig.cause, capture),
  272. 283 fix: sig.fix.map((f) => fill(f, capture)),
  273. 284 });
  274. 285 break;
  275. 286 }
  276. 287 }
  277. 288 matches.sort((a, b) => a.line - b.line);
  278. 289 // Missing modules and env vars usually explain later prerender, type and command errors.
  279. 290 const primary = matches[0] ?? null;
  280. 291 let firstError = null;
  281. 292 for (let i = 0; i < lines.length; i++) {
  282. 293 if (GENERIC_ERROR.test(lines[i]) && !NOISE.test(lines[i])) {
  283. 294 firstError = { line: i + 1, text: mask(lines[i].trim()).slice(0, 300) };
  284. 295 break;
  285. 296 }
  286. 297 }
  287. 298 return { detected: detect(log), lines: lines.length, primary: primary?.id ?? null, first_error: firstError, matches };
  288. 301function toMarkdown(r) {
  289. 302 const d = r.detected;
  290. 303 const out = [
  291. 304 "# Deploy triage",
  292. 305 "",
  293. 306 `Platform: ${d.platform} | Framework: ${d.framework} | Package manager: ${d.package_manager} | Node: ${d.node ?? "not shown"}`,
  294. 307 "",
  295. 308 ];
  296. 309 if (r.first_error) out.push(`First error line (${r.first_error.line}): \`${r.first_error.text.replace(/`/g, "'")}\``, "");
  297. 310 if (!r.matches.length) {
  298. 311 out.push("No known failure pattern matched. Read the lines around the first error above, and paste 30 lines before it if this is a partial log.", "");
  299. 312 return out.join("\n");
  300. 313 }
  301. 314 r.matches.forEach((m, i) => {
  302. 315 out.push(`## ${i === 0 ? "Most likely cause" : `Also seen`}: ${m.title}`, "", `Line ${m.line}: \`${m.excerpt.replace(/`/g, "'")}\``, "", m.cause, "", "Fix:", "");
  303. 316 m.fix.forEach((f, j) => out.push(`${j + 1}. ${f}`));
  304. 317 out.push("");
  305. 318 });
  306. 319 return out.join("\n");
  307. 322function write(path, text) {
  308. 323 mkdirSync(dirname(path), { recursive: true });
  309. 324 writeFileSync(path, text);
  310. 327function main() {
  311. 328 const args = parseArgs(process.argv.slice(2));
  312. 329 let log;
  313. 330 if (args.input === "-") log = readFileSync(0, "utf8");
  314. 331 else if (!existsSync(args.input)) {
  315. 332 console.error(`No log at ${args.input}. Save the failed build log there (copy it from the deploy page), or pipe it in with "-".`);
  316. 333 process.exit(2);
  317. 334 } else log = readFileSync(args.input, "utf8");
  318. 335 if (!log.trim()) {
  319. 336 console.error("The log is empty.");
  320. 337 process.exit(2);
  321. 338 }
  322. 339 const report = triage(log);
  323. 340 const md = toMarkdown(report);
  324. 341 if (args.json) write(args.json, JSON.stringify(report, null, 2) + "\n");
  325. 342 if (args.md) write(args.md, md);
  326. 343 if (!args.json && !args.md) console.log(md);
  327. 344 else console.log(`${report.matches.length} known patterns. Most likely: ${report.primary ?? "none matched"}.`);
  328. 347main();