vercel/turborepo · error · Error

Invalid version: ${version}

Error message

Invalid version: ${version}

What it means

The update-versioned-schema-json codemod rewrites turbo.json `$schema` URLs to versioned ones (https://<major-minor-patch>.turborepo.dev/schema.json). getVersionedSchemaUrl first tries semver.parse (preserving prerelease) then semver.coerce (lenient); when both return null the input contains no extractable version and it throws `Invalid version: <version>`.

Source

Thrown at packages/turbo-codemod/src/transforms/update-versioned-schema-json.ts:104

 * Replaces dots with hyphens to create a valid DNS subdomain.
 * e.g., "2.7.5" -> "v2-7-5", "2.9.4-canary.5" -> "v2-9-4-canary-5"
 */
function versionToSubdomain(version: string): string {
  return `v${version.replaceAll(".", "-")}`;
}

/**
 * Generates the new versioned schema URL.
 * Uses semver.parse to preserve prerelease identifiers (e.g., canary.5),
 * falling back to semver.coerce for non-standard version strings.
 */
export function getVersionedSchemaUrl(version: string): string {
  // parse preserves prerelease: "2.9.4-canary.5" -> "2.9.4-canary.5"
  // coerce strips it: "2.9.4-canary.5" -> "2.9.4"
  const parsed = parse(version);
  const resolved = parsed ? parsed.version : coerce(version)?.version;
  if (!resolved) {
    throw new Error(`Invalid version: ${version}`);
  }
  const subdomain = versionToSubdomain(resolved);
  return `https://${subdomain}.turborepo.dev/schema.json`;
}

/**
 * Updates any old schema URLs in file content to the new versioned URL
 */
function updateSchemaUrls(content: string, newUrl: string): string {
  let updated = content;
  // Replace static old URLs
  for (const oldUrl of OLD_SCHEMA_URLS) {
    updated = updated.replaceAll(oldUrl, newUrl);
  }
  // Replace outdated versioned URLs (e.g., v2-7-4 -> v2-7-5)
  updated = updated.replaceAll(VERSIONED_SCHEMA_URL_REGEX, newUrl);
  return updated;
}

View on GitHub (pinned to 9f94a7d215)

Solutions

  1. Set devDependencies.turbo to a concrete published version before running the codemod (e.g. `^2.3.0`), then retry
  2. If you must keep `workspace:*` day-to-day, temporarily pin a real version, run the codemod, then restore your alias — the schema URL it writes stays valid
  3. If invoking the transform programmatically, pass a parseable version string (semver.parse it first) instead of a range/alias

Example fix

// package.json (before)
"devDependencies": { "turbo": "workspace:*" }
// after
"devDependencies": { "turbo": "^2.3.0" }
Defensive patterns

Strategy: type-guard

Validate before calling

import { parse, coerce } from 'semver';

function isResolvableVersion(v: string | undefined): boolean {
  return Boolean(v) && Boolean(parse(v!) ?? coerce(v));
}
// before running the codemod: read devDependencies.turbo and require isResolvableVersion

Type guard

import { parse, coerce } from 'semver';

function isSemverResolvable(version: string): boolean {
  return Boolean(parse(version) ?? coerce(version));
}

Try / catch

try {
  getVersionedSchemaUrl(turboVersion);
} catch (err) {
  if (err instanceof Error && err.message.startsWith('Invalid version:')) {
    // fall back to the concrete version from the lockfile/registry instead of an alias
    return getVersionedSchemaUrl(await resolveInstalledTurboVersion());
  }
  throw err;
}

Prevention

When it happens

Trigger: A version string with no semver-shaped digits at all: `workspace:*`, `latest`, `file:../turbo`, `github:vercel/turbo`, `link:...`, or an empty string — anything semver.coerce cannot find numbers in.

Common situations: Monorepos aliasing turbo to a local workspace build (`workspace:*`, common in pnpm/Yarn setups); tarball or git dependencies for turbo; overridden resolutions replacing the real version.

Related errors


AI-assisted analysis of vercel/turborepo@9f94a7d215 (2026-08-16). Data as JSON: /api/errors/e56dadd4700ede9b. Report an issue: GitHub.