Version management in pipelines

Hello friends! I am quite new to Rust so I am sorry if I mix up any jargon or if my questions seem juvenile.

I am currently helping a team with making a rust pipeline where it lints, packages, publishes, builds, tests, etc an entire workspace. I am currently working on versioning the finished crate that gets published into kellnr since I am in a space that requires private and controlled registries (no internet access). It is bumping up the version associated with the crate based off of the version supplied/attached in the Cargo.toml. I guess my question is both:

  1. Will a Cargo.toml always "reference itself" as a crate/lib/worksspace?
  2. Does this leave the Cargo.toml permenantly behind the published crate by one version? Ideally, I wouldn't want to leave it up to the dev to update the version. I would want the version in the Cargo.toml to match the version of the crate that's published to kellnr. But I also am a bit sketched out to fit clone and write to the Cargo.toml within the same pipeline.

I was wondering if anyone else has experience with this. I am using Gitlab and we are building components specifically so that this Rust pipeline can be used as a component universally everywhere with various specs in the template to allow costumization and edge cases per repo as needed.

Thanks again in advance, I lik forward to continueing to learn more about Rust!

I can't tell what you're trying to ask here. Are you asking whether a Cargo.toml file, by itself, contains enough information to determine whether it's for a binary vs. a library vs. a workspace? I suspect that's not your question, but it's the only meaning I can read into what you wrote.

I'm assuming that your pipeline edits the version in Cargo.toml in its own working copy of the source but doesn't push the changes back to the central repository, and so the central repository version ends up behind. Is that what's going on? The simplest solution would be to commit the version change and push it, but presumably there's a reason you haven't done that that's specific to the pipeline technology you're using, so we'd have to know more about that to help.

Hey thanks for your response. Apologies if I am a bit verbose with the following, just want to make sure I provide enough context to make sense!

My current test structure is like this:

workspace/

  • Cargo.toml
  • /crate1
    • Cargo.toml
    • /src
      • lib.rs
      • main.rs
  • /crate2
    • "" (same as crate 1 struct)

The root Cargo.toml looks like

[workspace]
members = [
   crate1
   crate2
]

And the Cargo.toml's for each crate:

[package]
name = "safe_crate_1"
version = "0.1.1"
edition = "2021"

[lib]
name = "safe"
path = "src/lib.rs"

[[bin]]
name = "safe"
path = "src/main.rs"

Here, the package's version is what I am using to control versioning. So in this case, when the pipeline runs my python script reads the Cargo.toml and increments the version to 0.1.2. I do this by using cargo package. Here is a small glimpse of context.

new_version = ensure_and_bump_version(member_manifest)

        cmd = f"cargo package --manifest-path {member_manifest}"
        print(f"Running packaging command: {cmd}")
        result = c.run(cmd, warn=True)
        if result.exited != 0:
            print(f"cargo package failed for {member_manifest} with exit code {result.exited}")
            exit_code = result.exited
        else:
            print(f"cargo package succeeded for {member_manifest} with version {new_version}")
            packaged_manifests.append(member_manifest)

    def collect_crate_files() -> list[str]:
        package_dir = os.path.join(manifest_dir, "target", "package")
        if not os.path.exists(package_dir):
            print(f"Package directory {package_dir} does not exist")
            return []
        return glob.glob(os.path.join(package_dir, "*.crate"))

    # Determine whether this manifest is a workspace root
    with c.cd(manifest_dir):
       metadata_cmd = f"cargo metadata --format-version 1 --no-deps --manifest-path {manifest_path}"
        print(f"Reading workspace metadata: {metadata_cmd}")
        metadata_result = c.run(metadata_cmd, warn=True, hide=True)

then my versioning is done with toml

def ensure_and_bump_version(manifest_path: str, default_version: str = "0.1.0") -> str:
    """
    Read version from Cargo.toml.
    If missing or empty, set to default_version.
    Else bump patch version.
    Update Cargo.toml with new version.
    Return new version string.
    """
    with open(manifest_path, "r") as f:
        cargo_data = toml.load(f)

    if "package" not in cargo_data:
        raise RuntimeError(f"No [package] section in {manifest_path}")

    old_version = cargo_data["package"].get("version", "").strip()

    if not old_version:
        new_version = default_version
        print(f"No version found in {manifest_path}, setting to default {default_version}")
    else:
        v = Version(old_version)
        new_version = f"{v.major}.{v.minor}.{v.micro + 1}"
        print(f"Bumping version in {manifest_path}: {old_version} -> {new_version}")

    cargo_data["package"]["version"] = new_version

    with open(manifest_path, "w") as f:
        toml.dump(cargo_data, f)

    return new_version

In this situation, since the source of truth is the Cargo.toml for the version/determining if we are bumping, will it always define itself in this way when a user is trying to publish crates? Can the version always be sourced because in order to publish the Cargo.toml defines the [package]?


Currently I am not updating the version in the Cargo.toml when a new crate is published because I wanted to see if there alternatives to avoid doing a whole cycle of clone, write, push within the pipeline itself in order to keep the end package/crate and associated Cargo.toml in sync. I am a little confused by the central repository phrasing. What I think about is chicken before the egg scenario, where we are merging into main, we get a new version for the crate we package and publish to our registry but the Cargo.toml remains a version behind.


Hopefully that makes more sense. Gitlab Components are super powerful and helpful, but versioning has been a struggle in other areas, for example we also build the docker image in the same pipeline that downstream jobs use and versioning with that has been a nightmare but there has not been a writing back to the versions source of truth.

A Cargo package always has a Cargo.toml file with a [package] table, and a package's version always comes from the version key in that [package] table, with one possible exception: When using a workspace, it is possible to define a workspace-wide version in workspace.package.version in the workspace's Cargo.toml, and then this version is used by any package that sets package.version to { workspace = true }. The foolproof way to resolve a package's version string to handle this and any future variations is to run cargo metadata and parse the output.

Also, it's possible for a package not to have a version key at all, in which case the version is implicitly set to "0.0.0" and the package is considered ineligible for cargo publish as though package.publish was false.

Does that answer your question?

Hey

This does answer my question. Thank you. I spoke with my team lead and then we met with the other team and they recommended the route of using workspaces (cargo ws) to control versioning.

Thank you for taking the time to help me understand a bit better!