BigPizzaV3/CodexPlusPlus · error · anyhow::Error

Invalid version tag: {value}

Error message

Invalid version tag: {value}

What it means

parse_version_tag strips an optional leading v/V, then consumes the longest prefix made only of ASCII digits and dots; if nothing remains it bails with "Invalid version tag: {value}". It is called by is_newer_version(candidate, current) — and therefore by check_for_update — for both the remote release version and the locally compiled-in version. Any string that does not start with a digit after the optional v prefix fails: "dev", "latest", "nightly", "beta2", "".

Source

Thrown at crates/codex-plus-core/src/update.rs:56

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct UpdateInstall {
    pub release: Release,
    pub installer_path: PathBuf,
    pub launched: bool,
}

pub fn parse_version_tag(value: &str) -> anyhow::Result<Vec<u64>> {
    let normalized = value.trim().trim_start_matches(['v', 'V']);
    let mut digits = String::new();
    for ch in normalized.chars() {
        if ch.is_ascii_digit() || ch == '.' {
            digits.push(ch);
        } else {
            break;
        }
    }
    if digits.is_empty() {
        anyhow::bail!("Invalid version tag: {value}");
    }
    digits
        .split('.')
        .map(|part| part.parse::<u64>().map_err(Into::into))
        .collect()
}

pub fn is_newer_version(candidate: &str, current: &str) -> anyhow::Result<bool> {
    let mut left = parse_version_tag(candidate)?;
    let mut right = parse_version_tag(current)?;
    let len = left.len().max(right.len());
    left.resize(len, 0);
    right.resize(len, 0);
    Ok(left > right)
}

pub fn release_from_github_payload(payload: &Value) -> anyhow::Result<Release> {
    let version = payload

View on GitHub (pinned to 1f431ae49b)

Solutions

  1. Ensure both strings handed to is_newer_version look like vX.Y.Z with a leading digit after the optional v prefix
  2. Pass the real release tag from latest.json / GitHub instead of a branch or build-channel name
  3. If arbitrary strings can reach the call, pre-normalize: extract the first digit-and-dot run or default to "0"
  4. In UI flows, treat a parse failure as 'no update available' instead of propagating the error

Example fix

// before
let newer = is_newer_version(&release.version, current_version)?;

// after
fn safe_is_newer(candidate: &str, current: &str) -> bool {
    is_newer_version(candidate, current).unwrap_or(false)
}
let newer = safe_is_newer(&release.version, current_version);
Defensive patterns

Strategy: validation

Validate before calling

fn is_parseable_version_tag(value: &str) -> bool {
    value
        .trim()
        .trim_start_matches(['v', 'V'])
        .chars()
        .next()
        .is_some_and(|c| c.is_ascii_digit())
}

assert!(is_parseable_version_tag("v1.2.3"));
assert!(!is_parseable_version_tag("dev"));

Prevention

When it happens

Trigger: is_newer_version("dev", "0.3.1"), check_for_update("0.0.0-dev"), or parse_version_tag("v") — no digits survive normalization. Tags like "v1.2.3-rc.1" are accepted (parsed as 1.2.3; iteration stops at the first non-digit).

Common situations: Local/debug builds stamped with channel-style versions; a latest.json whose version field holds a name instead of a number; a release tagged "main"; CI passing a branch name instead of the tag into the update check.

Related errors


AI-assisted analysis of BigPizzaV3/CodexPlusPlus@1f431ae49b (2026-08-16). Data as JSON: /api/errors/4d0a289b586fdac4. Report an issue: GitHub.