dbt-labs/dbt-core · error · anyhow

expected X.Y.Z, got {base:?} in {semver:?}

Error message

expected X.Y.Z, got {base:?} in {semver:?}

What it means

parse_release_version requires the base (non-prerelease) part of the version to be exactly three non-empty all-digit components (X.Y.Z). This bail fires when the base doesn't match that shape, e.g. missing components, extra components, or non-numeric parts.

Source

Thrown at crates/dbt-ci/src/release_version.rs:59

        }
    }
}

pub(crate) fn parse_release_version(semver: &str) -> Result<ParsedReleaseVersion<'_>> {
    if semver.contains('+') {
        bail!("SemVer build metadata not supported in {semver:?}");
    }
    let (base, pre) = match semver.split_once('-') {
        Some((b, p)) => (b, Some(p)),
        None => (semver, None),
    };
    let parts: Vec<&str> = base.split('.').collect();
    let well_formed = parts.len() == 3
        && parts
            .iter()
            .all(|p| !p.is_empty() && p.chars().all(|c| c.is_ascii_digit()));
    if !well_formed {
        bail!("expected X.Y.Z, got {base:?} in {semver:?}");
    }
    let Some(pre) = pre else {
        return Ok(ParsedReleaseVersion {
            base,
            prerelease: None,
        });
    };
    let (lane_str, number) = pre
        .split_once('.')
        .ok_or_else(|| anyhow!("expected `<lane>.N` after `-`, got {pre:?} in {semver:?}"))?;
    let lane = PreReleaseLane::parse(lane_str).ok_or_else(|| {
        anyhow!(
            "unsupported pre-release lane {lane_str:?} in {semver:?}; \
             accepted: alpha, beta, rc, preview, dev"
        )
    })?;
    if number.is_empty() || !number.chars().all(|c| c.is_ascii_digit()) {
        bail!("expected integer after `{lane_str}.`, got {number:?} in {semver:?}");

View on GitHub (pinned to 0267ce9170)

Solutions

  1. Change the version to exactly three numeric components, e.g. "1.2.3".
  2. Strip any leading "v" prefix or other decoration before passing the version.
  3. Fix the CI/release tooling so it emits a full three-part version.

Example fix

// before
parse_release_version("v1.2")?;
// after
let v = version.trim_start_matches('v');
let v = format!("{}.0", v); // "1.2" -> "1.2.0"
parse_release_version(&v)?;
Defensive patterns

Strategy: validation

Validate before calling

fn validate_semver_base(v: &str) -> Result<(), String> {
    let base = v.trim_start_matches('v').split('-').next().unwrap();
    let ok = base.split('.').count() == 3
        && base.split('.').all(|p| !p.is_empty() && p.chars().all(|c| c.is_ascii_digit()));
    if ok { Ok(()) } else { Err(format!("{v:?} is not X.Y.Z")) }
}

Try / catch

let v = version.trim_start_matches('v');
parse_release_version(v).map_err(|e| {
    eprintln!("bad release version {v:?}: {e}");
    e
})?;

Prevention

When it happens

Trigger: Calling parse_release_version (or validate_release_version / semver_to_pep440) with versions like "1.2", "1.2.3.4", "v1.2.3", "1.2.x", or "1.2.3-" whose base is not three integer components.

Common situations: Hand-edited version strings in release configs, a leading 'v' prefix copied from a git tag, two-component versions from simplified versioning schemes, or template placeholders left unsubstituted in CI.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


AI-assisted analysis of dbt-labs/dbt-core@0267ce9170 (2026-09-07). Data as JSON: /api/errors/786f1cb22db3d01b. Report an issue: GitHub.