Skip to content

Embed release version in source instead of relying on CI string replacement #976

@qweered

Description

@qweered

Problem

The vp binary's version is hardcoded as "0.0.0" in crates/vite_global_cli/Cargo.toml (and all other workspace crates). The actual release version is injected by CI via string replacement:

# .github/workflows/release.yml
pnpm exec tool replace-file-content crates/vite_global_cli/Cargo.toml 'version = "0.0.0"' 'version = "${{ env.VERSION }}"'

This means anyone building from a tagged source tarball (e.g. v0.1.12) gets vp v0.0.0:

$ vp --version
vp v0.0.0

This affects:

  • Distro packagers (Nix, Homebrew, AUR, etc.) who build from source tarballs/fetchFromGitHub
  • vp upgrade --check — compares env!("CARGO_PKG_VERSION") against the registry, so 0.0.0 always appears outdated
  • vp upgrade --rollback — displays 0.0.0 as the current version in status messages

All three env!("CARGO_PKG_VERSION") call sites are affected:

  • crates/vite_global_cli/src/commands/version.rs:151
  • crates/vite_global_cli/src/commands/upgrade/mod.rs:67
  • crates/vite_global_cli/src/commands/upgrade/mod.rs:229

Current workaround for packagers

Nix (and likely any other distro) must replicate the CI string replacement:

sed -i 's/version = "0.0.0"/version = "0.1.12"/' crates/vite_global_cli/Cargo.toml

This is fragile — it only works because vite_global_cli is the only crate where CARGO_PKG_VERSION is read at compile time. If another crate starts using it, the packager's sed breaks silently.

Suggested fix

Use a build.rs that derives the version from the git tag at build time, with a fallback to an environment variable for CI/packager overrides:

crates/vite_global_cli/build.rs:

fn main() {
    // Allow CI or packagers to override via environment variable
    if let Ok(version) = std::env::var("VITE_PLUS_VERSION") {
        println!("cargo:rustc-env=VITE_PLUS_VERSION={version}");
        return;
    }

    // Try to read version from git tag
    if let Ok(output) = std::process::Command::new("git")
        .args(["describe", "--tags", "--match", "v*", "--abbrev=0"])
        .output()
    {
        if output.status.success() {
            let tag = String::from_utf8_lossy(&output.stdout)
                .trim()
                .trim_start_matches('v')
                .to_string();
            if !tag.is_empty() {
                println!("cargo:rustc-env=VITE_PLUS_VERSION={tag}");
                return;
            }
        }
    }

    // Fallback to CARGO_PKG_VERSION (0.0.0 in dev)
    println!(
        "cargo:rustc-env=VITE_PLUS_VERSION={}",
        std::env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| "0.0.0".to_string())
    );
}

Then replace env!("CARGO_PKG_VERSION") with env!("VITE_PLUS_VERSION") in the three call sites.

This gives three resolution layers:

  1. VITE_PLUS_VERSION env var — CI and distro packagers set this explicitly
  2. git describe — works for local dev builds from a cloned repo
  3. CARGO_PKG_VERSION — fallback to 0.0.0 in dev (same as today)

The CI workflow's replace-file-content step can then be replaced with:

env:
  VITE_PLUS_VERSION: ${{ needs.prepare.outputs.version }}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions