{"record":{"id":"bbe1ad98f05881a2","repo":"mui/material-ui","slug":"failed-to-update-package-versions","errorCode":null,"errorMessage":"Failed to update package versions","messagePattern":"Failed to update package versions","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/canaryRelease.mts","lineNumber":168,"sourceCode":"  const { stdout: commitTimestamp } = await $`git show --no-patch --format=%ct HEAD`;\n  const timestamp = formatDate(new Date(+commitTimestamp * 1000));\n  let hasError = false;\n\n  const tasks = packages.map(async (pkg) => {\n    const packageJsonPath = resolve(pkg.path, './package.json');\n    try {\n      const packageJson = JSON.parse(await readFile(packageJsonPath, { encoding: 'utf8' }));\n      packageJson.version = `${packageJson.version}-dev.${timestamp}-${currentRevisionSha}`;\n      await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\\n');\n    } catch (error) {\n      console.error(`${chalk.red(`❌ ${packageJsonPath}`)}`, error);\n      hasError = true;\n    }\n  });\n\n  await Promise.allSettled(tasks);\n  if (hasError) {\n    throw new Error('Failed to update package versions');\n  }\n}\n\nfunction formatDate(date: Date) {\n  // yyyyMMdd-HHmmss\n  return date\n    .toISOString()\n    .replace(/[-:Z.]/g, '')\n    .replace('T', '-')\n    .slice(0, 15);\n}\n\nfunction buildPackages() {\n  if (process.env.CI) {\n    return $$`pnpm build:public:ci`;\n  }\n\n  return $$`pnpm build:public`;","sourceCodeStart":150,"sourceCodeEnd":186,"githubUrl":"https://github.com/mui/material-ui/blob/bdc96df2cb530fcdd60a7a7aabe37f610ce0f0a5/scripts/canaryRelease.mts#L150-L186","documentation":"Thrown by setVersion() in scripts/canaryRelease.mts after Promise.allSettled() completes if any per-package task set hasError=true. Each individual failure (read/parse/write of a package's package.json) is already logged to stderr with its path and the underlying error before this aggregate is raised. It exists so the canary publish aborts rather than shipping packages with stale or partially-updated versions. The thrown message itself does not list which packages failed — that context only appears in the preceding console output.","triggerScenarios":"One of the parallel tasks in packages.map(...) rejected: readFile failed because pkg.path does not point at a real package.json; JSON.parse failed because the file is malformed or empty; packageJson.version was undefined and produced an invalid canary string; or writeFile failed due to permissions, read-only mount, or disk full. hasError flips to true in that task's catch block, then the post-allSettled guard throws.","commonSituations":"A newly added workspace whose path reported by `pnpm list --recursive --json` does not match where its package.json actually lives; a package.json left corrupt by a previous interrupted run (the script's own cleanUp does `git restore .` in finally, but a crash before finally leaves the tree dirty); CI runners with restricted write permissions on the workspace; an empty or hand-edited package.json committed by mistake.","solutions":["Scroll up in the log: each failing path was already printed as `❌ <packageJsonPath>` together with the underlying error. Fix that file first.","For each changed package, confirm the path from `pnpm list --recursive --filter ...[<baseline>] --depth -1 --only-projects --json` actually contains a valid package.json (e.g. `node -e \"JSON.parse(require('fs').readFileSync('<path>/package.json','utf8'))\"`).","Restore a clean tree before re-running: `git restore .` (the script enforces a clean working directory at start via ensureCleanWorkingWindow).","On CI, verify the runner has write access to the workspace and that the pnpm workspace catalog is up to date."],"exampleFix":"// before\nlet hasError = false;\nconst tasks = packages.map(async (pkg) => {\n  const packageJsonPath = resolve(pkg.path, './package.json');\n  try {\n    /* ...read + bump + write... */\n  } catch (error) {\n    console.error(`${chalk.red(`❌ ${packageJsonPath}`)}`, error);\n    hasError = true;\n  }\n});\nawait Promise.allSettled(tasks);\nif (hasError) {\n  throw new Error('Failed to update package versions');\n}\n\n// after\nconst failed: string[] = [];\nconst tasks = packages.map(async (pkg) => {\n  const packageJsonPath = resolve(pkg.path, './package.json');\n  try {\n    /* ...read + bump + write... */\n  } catch (error) {\n    console.error(`${chalk.red(`❌ ${packageJsonPath}`)}`, error);\n    failed.push(packageJsonPath);\n  }\n});\nawait Promise.allSettled(tasks);\nif (failed.length > 0) {\n  throw new Error(`Failed to update package versions for: ${failed.join(', ')}`);\n}","handlingStrategy":"validation","validationCode":"// Run before calling the canary release flow.\n// Rejects early if any package's package.json is missing or unparseable,\n// so setVersion() never reaches the aggregate throw.\nimport { readFile } from 'node:fs/promises';\nimport { resolve } from 'node:path';\n\nasync function assertPackagesWritable(packages: { path: string; name: string }[]) {\n  const failures: string[] = [];\n  await Promise.all(packages.map(async (pkg) => {\n    const p = resolve(pkg.path, 'package.json');\n    try {\n      const raw = await readFile(p, { encoding: 'utf8' });\n      const json = JSON.parse(raw);\n      if (typeof json.version !== 'string' || json.version.length === 0) {\n        failures.push(`${p}: missing or non-string \"version\"`);\n      }\n    } catch (err) {\n      failures.push(`${p}: ${(err as Error).message}`);\n    }\n  }));\n  if (failures.length > 0) {\n    throw new Error(`Pre-flight package.json check failed:\\n  - ${failures.join('\\n  - ')}`);\n  }\n}","typeGuard":"import type { PackageInfo } from './canaryRelease';\n\n// Narrows a parsed package.json to one that setVersion() can safely mutate.\nfunction isBumpablePackageJson(\n  value: unknown,\n): value is { version: string; [k: string]: unknown } {\n  if (typeof value !== 'object' || value === null) return false;\n  const v = (value as { version?: unknown }).version;\n  return typeof v === 'string' && v.length > 0 && /^\\d+\\.\\d+\\.\\d+/.test(v);\n}","tryCatchPattern":"// Callers of the release flow: catch the aggregate, then re-surface the\n// already-printed per-package failures explicitly so CI logs are actionable.\ntry {\n  await setVersion(changedPackages);\n} catch (err) {\n  if (err instanceof Error && err.message === 'Failed to update package versions') {\n    throw new Error(\n      'Aborted canary release: one or more package.json files could not be bumped. ' +\n      'See the per-package `❌ <path>` lines above for the underlying cause.',\n    );\n  }\n  throw err;\n}","preventionTips":["Run the script from a clean working tree so ensureCleanWorkingWindow does not bail and partial state cannot accumulate.","Keep the pnpm workspace catalog accurate: every package returned by `pnpm list --recursive --json` must point at a real, valid package.json.","In CI, fail fast on `pnpm install` issues before invoking canary release, so a bad install does not masquerade as a bump failure.","If you extend the per-package task, prefer collecting failed paths into an array and including them in the thrown message rather than relying solely on console output."],"tags":["release-automation","monorepo","package-json","canary","aggregate-error"],"backgroundTag":null,"analyzedSha":"bdc96df2cb530fcdd60a7a7aabe37f610ce0f0a5","analyzedAt":"2026-08-12T22:59:56.717Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}