feat(release): bump the root crate in Cargo.toml and Cargo.lock

A repo whose Cargo.toml has a [package] section now gets its version set to the release version, and the matching local [[package]] entry in Cargo.lock (same name, no source line) follows in the same release commit, so cargo build --locked keeps passing. Workspaces without [package] and repos without Cargo.toml release as before; a Cargo.lock without the local entry aborts the release before any file is written.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kevin Veen-BirkenbachandClaude Opus 5.5 committed 2026-10-31 14:18:07 +01:00
1 parent 168d59e1b2
commit d823a42edc
4 files changed
+194

No files matched your search

@@ -10,6 +10,7 @@ Keep this package stable so existing imports continue to work, e.g.:
from __future__ import annotations
from .cargo import update_cargo_version
from .changelog_md import update_changelog
from .debian import _get_debian_author, update_debian_changelog
from .editor import _open_editor_for_changelog
@@ -22,6 +23,7 @@ from .rpm_spec import update_spec_version
__all__ = [
"_get_debian_author",
"_open_editor_for_changelog",
"update_cargo_version",
"update_changelog",
"update_debian_changelog",
"update_flake_version",
+76
View File
@@ -0,0 +1,76 @@
from __future__ import annotations
import os
import re
def _read(path: str) -> str:
with open(path, encoding="utf-8") as f:
return f.read()
def _write(path: str, content: str) -> None:
with open(path, "w", encoding="utf-8") as f:
f.write(content)
def update_cargo_version(
repo_root: str, new_version: str, preview: bool = False
) -> list[str]:
"""
Set the root crate's version in Cargo.toml and its entry in Cargo.lock.
Args:
repo_root: repository root that may hold Cargo.toml and Cargo.lock.
new_version: release version without the leading "v".
preview: only print what would change.
Returns:
The paths that were (or in preview would be) changed.
"""
toml_path = os.path.join(repo_root, "Cargo.toml")
if not os.path.exists(toml_path):
print("[INFO] No Cargo.toml found. Skipping Cargo version update.")
return []
toml = _read(toml_path)
block = re.search(r"(?ms)^\[package\]\s*$.*?(?=^\s*\[|\Z)", toml)
if not block:
print("[INFO] Cargo.toml has no [package] section. Skipping.")
return []
name = re.search(r'(?m)^\s*name\s*=\s*"([^"]+)"', block.group(0))
if not name:
raise RuntimeError("Missing name key in [package] section of Cargo.toml")
new_block, count = re.subn(
r'(?m)^(\s*version\s*=\s*")[^"]+(")',
lambda m: f"{m.group(1)}{new_version}{m.group(2)}",
block.group(0),
count=1,
)
if count == 0:
raise RuntimeError("Missing version key in [package] section of Cargo.toml")
changes = {toml_path: toml[: block.start()] + new_block + toml[block.end() :]}
lock_path = os.path.join(repo_root, "Cargo.lock")
if os.path.exists(lock_path):
lock, count = re.subn(
rf'(?m)^(\[\[package\]\]\nname = "{re.escape(name.group(1))}"\n'
r'version = ")[^"]+("\n)(?!source = )',
lambda m: f"{m.group(1)}{new_version}{m.group(2)}",
_read(lock_path),
)
if count != 1:
raise RuntimeError(
f"Expected one local {name.group(1)!r} package in Cargo.lock, "
f"found {count}"
)
changes[lock_path] = lock
for path, content in changes.items():
if preview:
print(f"[PREVIEW] Would update {path} version to {new_version}")
else:
_write(path, content)
print(f"Updated {path} version to {new_version}")
return list(changes)
+4
View File
@@ -10,6 +10,7 @@ from pkgmgr.core.git.queries import get_current_branch
from pkgmgr.core.repository.paths import resolve_repo_paths
from .files import (
update_cargo_version,
update_changelog,
update_debian_changelog,
update_flake_version,
@@ -74,6 +75,8 @@ def _release_impl(
update_flake_version(paths.flake_nix, new_ver_str, preview=preview)
cargo_files = update_cargo_version(repo_root, new_ver_str, preview=preview)
if paths.arch_pkgbuild:
update_pkgbuild_version(paths.arch_pkgbuild, new_ver_str, preview=preview)
else:
@@ -124,6 +127,7 @@ def _release_impl(
paths.arch_pkgbuild,
paths.rpm_spec,
paths.debian_changelog,
*cargo_files,
]
existing_files = [
p for p in files_to_add if isinstance(p, str) and p and os.path.exists(p)
@@ -0,0 +1,112 @@
from __future__ import annotations
import os
import tempfile
import unittest
from pkgmgr.actions.release.files import update_cargo_version
CARGO_TOML = """[package]
name = "security-check"
version = "0.1.0"
edition = "2024"
[dependencies]
serde = { version = "1.0.0" }
"""
CARGO_LOCK = """version = 4
[[package]]
name = "security-check"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abc"
[[package]]
name = "security-check"
version = "0.1.0"
dependencies = [
"serde",
]
[[package]]
name = "serde"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
"""
class TestUpdateCargoVersion(unittest.TestCase):
def setUp(self) -> None:
self._tmp = tempfile.TemporaryDirectory()
self.root = self._tmp.name
def tearDown(self) -> None:
self._tmp.cleanup()
def _write(self, name: str, content: str) -> str:
path = os.path.join(self.root, name)
with open(path, "w", encoding="utf-8") as f:
f.write(content)
return path
def _read(self, name: str) -> str:
with open(os.path.join(self.root, name), encoding="utf-8") as f:
return f.read()
def test_bumps_package_and_local_lock_entry_only(self) -> None:
toml = self._write("Cargo.toml", CARGO_TOML)
lock = self._write("Cargo.lock", CARGO_LOCK)
changed = update_cargo_version(self.root, "0.4.0")
self.assertEqual(changed, [toml, lock])
self.assertEqual(
self._read("Cargo.toml"),
CARGO_TOML.replace('version = "0.1.0"', 'version = "0.4.0"'),
)
self.assertEqual(
self._read("Cargo.lock"),
CARGO_LOCK.replace(
'name = "security-check"\nversion = "0.1.0"\ndependencies',
'name = "security-check"\nversion = "0.4.0"\ndependencies',
),
)
def test_without_cargo_toml_changes_nothing(self) -> None:
self.assertEqual(update_cargo_version(self.root, "0.4.0"), [])
self.assertEqual(os.listdir(self.root), [])
def test_workspace_without_package_is_untouched(self) -> None:
workspace = '[workspace]\nmembers = ["a"]\n'
self._write("Cargo.toml", workspace)
self.assertEqual(update_cargo_version(self.root, "0.4.0"), [])
self.assertEqual(self._read("Cargo.toml"), workspace)
def test_without_lock_bumps_only_toml(self) -> None:
toml = self._write("Cargo.toml", CARGO_TOML)
self.assertEqual(update_cargo_version(self.root, "0.4.0"), [toml])
def test_preview_writes_nothing(self) -> None:
self._write("Cargo.toml", CARGO_TOML)
self._write("Cargo.lock", CARGO_LOCK)
update_cargo_version(self.root, "0.4.0", preview=True)
self.assertEqual(self._read("Cargo.toml"), CARGO_TOML)
self.assertEqual(self._read("Cargo.lock"), CARGO_LOCK)
def test_lock_without_local_entry_fails_before_writing(self) -> None:
self._write("Cargo.toml", CARGO_TOML)
self._write("Cargo.lock", 'version = 4\n\n[[package]]\nname = "serde"\n')
with self.assertRaises(RuntimeError):
update_cargo_version(self.root, "0.4.0")
self.assertEqual(self._read("Cargo.toml"), CARGO_TOML)
if __name__ == "__main__":
unittest.main()