mirror of
https://github.com/storytold/craft-common-actions.git
synced 2026-11-15 15:02:24 +00:00
Merge pull request #234 from storytold/fix/cut-release-minor-bump
cut_release: a minor bump follows stale path versions and versioned lock names
This commit is contained in:
2 files changed
+200
-159
No files matched your search
+20
-3
@@ -104,6 +104,14 @@ def workspace_version(root: Path) -> str:
|
||||
raise ValueError("Cargo.toml has no [workspace.package] version")
|
||||
|
||||
|
||||
def _stale_path_version(line: str, old: str) -> str | None:
|
||||
"""The version a path dependency line names, when it is `old` or another patch of its minor."""
|
||||
m = re.search(r'version\s*=\s*"([^"]+)"', line)
|
||||
if not m or not SEMVER.match(m[1]):
|
||||
return None
|
||||
return m[1] if m[1].split(".")[:2] == old.split(".")[:2] else None
|
||||
|
||||
|
||||
def bump_cargo(root: Path, new: str, members: set[str] | None = None) -> tuple[str, int, int]:
|
||||
"""Set the workspace version to `new`. Returns (old version, Cargo.toml lines, lock packages).
|
||||
|
||||
@@ -126,12 +134,15 @@ def bump_cargo(root: Path, new: str, members: set[str] | None = None) -> tuple[s
|
||||
# Only the value: a trailing comment stays.
|
||||
lines[i] = re.sub(rf'^(version\s*=\s*"){re.escape(old)}"', lambda m: f'{m[1]}{new}"', line)
|
||||
changed += 1
|
||||
elif "path = " in line and f'version = "{old}"' in line:
|
||||
lines[i] = line.replace(f'version = "{old}"', f'version = "{new}"')
|
||||
elif "path = " in line and (stale := _stale_path_version(line, old)):
|
||||
# An internal dependency can sit at an earlier patch of the old minor (0.21.0 beside a
|
||||
# 0.21.1 workspace): it matched 0.21.1, and would not match the new version.
|
||||
lines[i] = line.replace(f'version = "{stale}"', f'version = "{new}"')
|
||||
changed += 1
|
||||
_write(root / "Cargo.toml", "\n".join(lines), crlf)
|
||||
|
||||
lock_path, packages = root / "Cargo.lock", 0
|
||||
bumped: set[str] = set()
|
||||
if lock_path.exists():
|
||||
lock, lcrlf = _read(lock_path)
|
||||
blocks = lock.split("\n[[package]]\n")
|
||||
@@ -147,9 +158,15 @@ def bump_cargo(root: Path, new: str, members: set[str] | None = None) -> tuple[s
|
||||
if f == f'version = "{old}"':
|
||||
fields[j] = f'version = "{new}"'
|
||||
packages += 1
|
||||
bumped.add(name)
|
||||
break
|
||||
blocks[b] = "\n".join(fields)
|
||||
_write(lock_path, "\n[[package]]\n".join(blocks), lcrlf)
|
||||
lock = "\n[[package]]\n".join(blocks)
|
||||
# Where another package of the same name is in the graph (the app's own crate from an older
|
||||
# git revision), dependency lists name the version too: `"vectorcraft-geom 0.24.1"`.
|
||||
for name in bumped:
|
||||
lock = lock.replace(f'\n "{name} {old}",\n', f'\n "{name} {new}",\n')
|
||||
_write(lock_path, lock, lcrlf)
|
||||
return old, changed, packages
|
||||
|
||||
|
||||
|
||||
+180
-156
@@ -1,156 +1,180 @@
|
||||
"""scripts/cut_release.py: the version bump a release commits, without git or gh."""
|
||||
import importlib.util
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
spec = importlib.util.spec_from_file_location("cut_release", ROOT / "scripts/cut_release.py")
|
||||
cut_release = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(cut_release)
|
||||
|
||||
CARGO_TOML = """[workspace]
|
||||
members = ["crates/*", "apps/demo"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.18.1"
|
||||
edition = "2024"
|
||||
|
||||
[workspace.dependencies]
|
||||
demo-geom = { path = "crates/geom", version = "0.18.1" }
|
||||
demo-io = { path = "crates/io", version = "0.18.1" }
|
||||
serde = { version = "0.18.1" }
|
||||
craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "abc" }
|
||||
"""
|
||||
|
||||
CARGO_LOCK = """# This file is automatically @generated by Cargo.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "demo"
|
||||
version = "0.18.1"
|
||||
dependencies = [
|
||||
"demo-geom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "demo-geom"
|
||||
version = "0.18.1"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.1.0"
|
||||
"""
|
||||
|
||||
|
||||
class NextVersion(unittest.TestCase):
|
||||
def test_minor_and_patch(self):
|
||||
self.assertEqual(cut_release.next_version("v0.20.0", "minor"), "0.21.0")
|
||||
self.assertEqual(cut_release.next_version("0.23.1", "minor"), "0.24.0")
|
||||
self.assertEqual(cut_release.next_version("v0.23.1", "patch"), "0.23.2")
|
||||
|
||||
def test_a_prerelease_patch_is_the_release_itself(self):
|
||||
self.assertEqual(cut_release.next_version("v0.3.0-rc.1", "patch"), "0.3.0")
|
||||
|
||||
def test_rejects_what_is_not_a_version(self):
|
||||
with self.assertRaises(ValueError):
|
||||
cut_release.next_version("nightly", "minor")
|
||||
|
||||
|
||||
class Versions(unittest.TestCase):
|
||||
def test_compared_as_numbers_with_prereleases_first(self):
|
||||
key = cut_release.version_key
|
||||
self.assertGreater(key("0.10.0"), key("0.9.0"))
|
||||
self.assertGreater(key("v0.22.0"), key("0.21.0"))
|
||||
self.assertLess(key("0.21.0-rc.1"), key("0.21.0"))
|
||||
self.assertEqual(key("0.21.0"), key("v0.21.0"))
|
||||
|
||||
|
||||
class Arguments(unittest.TestCase):
|
||||
def test_what_could_reach_git_or_gh_as_an_option_is_refused(self):
|
||||
ok = dict(repo="storytold/pdfcraft", version="0.21.0", commit="5ff567a4a2", workflow="CI", title="PdfCraft")
|
||||
cut_release.check_args(**ok)
|
||||
for field, bad in [("repo", "--upload-pack=x"), ("repo", "storytold"), ("version", "--force"),
|
||||
("version", "0.21"), ("commit", "--output=/tmp/x"), ("commit", "HEAD~1"),
|
||||
("workflow", "-x"), ("title", "-m evil"), ("title", "two\nlines")]:
|
||||
with self.assertRaises(SystemExit, msg=f"{field}={bad!r}"):
|
||||
cut_release.check_args(**{**ok, field: bad})
|
||||
|
||||
|
||||
class Bump(unittest.TestCase):
|
||||
def repo(self, toml=CARGO_TOML, lock=CARGO_LOCK, crlf=False):
|
||||
d = Path(tempfile.mkdtemp())
|
||||
nl = "\r\n" if crlf else "\n"
|
||||
(d / "Cargo.toml").write_bytes(toml.replace("\n", nl).encode())
|
||||
(d / "Cargo.lock").write_bytes(lock.replace("\n", nl).encode())
|
||||
return d
|
||||
|
||||
def test_bumps_the_workspace_its_path_dependencies_and_its_own_lock_entries(self):
|
||||
d = self.repo()
|
||||
old, lines, packages = cut_release.bump_cargo(d, "0.21.0")
|
||||
self.assertEqual((old, lines, packages), ("0.18.1", 3, 2))
|
||||
toml = (d / "Cargo.toml").read_text()
|
||||
self.assertIn('[workspace.package]\nversion = "0.21.0"', toml)
|
||||
self.assertIn('demo-geom = { path = "crates/geom", version = "0.21.0" }', toml)
|
||||
# A registry dependency that happens to share the version is not the workspace's.
|
||||
self.assertIn('serde = { version = "0.18.1" }', toml)
|
||||
lock = (d / "Cargo.lock").read_text()
|
||||
self.assertIn('name = "demo"\nversion = "0.21.0"', lock)
|
||||
self.assertIn('name = "demo-geom"\nversion = "0.21.0"', lock)
|
||||
self.assertIn('name = "serde"\nversion = "0.18.1"\nsource', lock)
|
||||
self.assertIn('name = "xtask"\nversion = "0.1.0"', lock)
|
||||
self.assertEqual(cut_release.workspace_version(d), "0.21.0")
|
||||
|
||||
def test_keeps_crlf_files_crlf(self):
|
||||
d = self.repo(crlf=True)
|
||||
cut_release.bump_cargo(d, "0.21.0")
|
||||
for name in ("Cargo.toml", "Cargo.lock"):
|
||||
raw = (d / name).read_bytes()
|
||||
self.assertNotIn(b"\n", raw.replace(b"\r\n", b""), name)
|
||||
|
||||
def test_refuses_a_bump_to_the_same_version_or_a_bad_one(self):
|
||||
d = self.repo()
|
||||
with self.assertRaises(ValueError):
|
||||
cut_release.bump_cargo(d, "0.18.1")
|
||||
with self.assertRaises(ValueError):
|
||||
cut_release.bump_cargo(d, "0.21")
|
||||
|
||||
def test_runs_the_repositorys_own_hook_with_old_and_new(self):
|
||||
d = self.repo()
|
||||
(d / "packaging").mkdir()
|
||||
(d / cut_release.HOOK).write_text("#!/bin/bash\n")
|
||||
calls = []
|
||||
old = cut_release.bump(d, "0.21.0", run=lambda cmd, **kw: calls.append((cmd, kw["cwd"])))
|
||||
self.assertEqual(old, "0.18.1")
|
||||
self.assertEqual(calls, [(["bash", cut_release.HOOK, "0.18.1", "0.21.0"], d)])
|
||||
|
||||
def test_a_comment_after_the_workspace_version_stays(self):
|
||||
d = self.repo(toml=CARGO_TOML.replace('version = "0.18.1"\nedition', 'version = "0.18.1" # the release\nedition'))
|
||||
cut_release.bump_cargo(d, "0.21.0")
|
||||
self.assertIn('version = "0.21.0" # the release', (d / "Cargo.toml").read_text())
|
||||
self.assertEqual(cut_release.workspace_version(d), "0.21.0")
|
||||
|
||||
def test_given_the_members_only_their_lock_entries_change(self):
|
||||
# A local path dependency outside the workspace keeps its version even when it matches.
|
||||
d = self.repo()
|
||||
_, _, packages = cut_release.bump_cargo(d, "0.21.0", members={"demo"})
|
||||
lock = (d / "Cargo.lock").read_text()
|
||||
self.assertEqual(packages, 1)
|
||||
self.assertIn('name = "demo"\nversion = "0.21.0"', lock)
|
||||
self.assertIn('name = "demo-geom"\nversion = "0.18.1"', lock)
|
||||
|
||||
def test_no_hook_no_call(self):
|
||||
d = self.repo()
|
||||
calls = []
|
||||
cut_release.bump(d, "0.21.0", run=lambda *a, **kw: calls.append(a))
|
||||
self.assertEqual(calls, [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
"""scripts/cut_release.py: the version bump a release commits, without git or gh."""
|
||||
import importlib.util
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
spec = importlib.util.spec_from_file_location("cut_release", ROOT / "scripts/cut_release.py")
|
||||
cut_release = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(cut_release)
|
||||
|
||||
CARGO_TOML = """[workspace]
|
||||
members = ["crates/*", "apps/demo"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.18.1"
|
||||
edition = "2024"
|
||||
|
||||
[workspace.dependencies]
|
||||
demo-geom = { path = "crates/geom", version = "0.18.1" }
|
||||
demo-io = { path = "crates/io", version = "0.18.1" }
|
||||
serde = { version = "0.18.1" }
|
||||
craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "abc" }
|
||||
"""
|
||||
|
||||
CARGO_LOCK = """# This file is automatically @generated by Cargo.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "demo"
|
||||
version = "0.18.1"
|
||||
dependencies = [
|
||||
"demo-geom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "demo-geom"
|
||||
version = "0.18.1"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.1.0"
|
||||
"""
|
||||
|
||||
|
||||
class NextVersion(unittest.TestCase):
|
||||
def test_minor_and_patch(self):
|
||||
self.assertEqual(cut_release.next_version("v0.20.0", "minor"), "0.21.0")
|
||||
self.assertEqual(cut_release.next_version("0.23.1", "minor"), "0.24.0")
|
||||
self.assertEqual(cut_release.next_version("v0.23.1", "patch"), "0.23.2")
|
||||
|
||||
def test_a_prerelease_patch_is_the_release_itself(self):
|
||||
self.assertEqual(cut_release.next_version("v0.3.0-rc.1", "patch"), "0.3.0")
|
||||
|
||||
def test_rejects_what_is_not_a_version(self):
|
||||
with self.assertRaises(ValueError):
|
||||
cut_release.next_version("nightly", "minor")
|
||||
|
||||
|
||||
class Versions(unittest.TestCase):
|
||||
def test_compared_as_numbers_with_prereleases_first(self):
|
||||
key = cut_release.version_key
|
||||
self.assertGreater(key("0.10.0"), key("0.9.0"))
|
||||
self.assertGreater(key("v0.22.0"), key("0.21.0"))
|
||||
self.assertLess(key("0.21.0-rc.1"), key("0.21.0"))
|
||||
self.assertEqual(key("0.21.0"), key("v0.21.0"))
|
||||
|
||||
|
||||
class Arguments(unittest.TestCase):
|
||||
def test_what_could_reach_git_or_gh_as_an_option_is_refused(self):
|
||||
ok = dict(repo="storytold/pdfcraft", version="0.21.0", commit="5ff567a4a2", workflow="CI", title="PdfCraft")
|
||||
cut_release.check_args(**ok)
|
||||
for field, bad in [("repo", "--upload-pack=x"), ("repo", "storytold"), ("version", "--force"),
|
||||
("version", "0.21"), ("commit", "--output=/tmp/x"), ("commit", "HEAD~1"),
|
||||
("workflow", "-x"), ("title", "-m evil"), ("title", "two\nlines")]:
|
||||
with self.assertRaises(SystemExit, msg=f"{field}={bad!r}"):
|
||||
cut_release.check_args(**{**ok, field: bad})
|
||||
|
||||
|
||||
class Bump(unittest.TestCase):
|
||||
def repo(self, toml=CARGO_TOML, lock=CARGO_LOCK, crlf=False):
|
||||
d = Path(tempfile.mkdtemp())
|
||||
nl = "\r\n" if crlf else "\n"
|
||||
(d / "Cargo.toml").write_bytes(toml.replace("\n", nl).encode())
|
||||
(d / "Cargo.lock").write_bytes(lock.replace("\n", nl).encode())
|
||||
return d
|
||||
|
||||
def test_bumps_the_workspace_its_path_dependencies_and_its_own_lock_entries(self):
|
||||
d = self.repo()
|
||||
old, lines, packages = cut_release.bump_cargo(d, "0.21.0")
|
||||
self.assertEqual((old, lines, packages), ("0.18.1", 3, 2))
|
||||
toml = (d / "Cargo.toml").read_text()
|
||||
self.assertIn('[workspace.package]\nversion = "0.21.0"', toml)
|
||||
self.assertIn('demo-geom = { path = "crates/geom", version = "0.21.0" }', toml)
|
||||
# A registry dependency that happens to share the version is not the workspace's.
|
||||
self.assertIn('serde = { version = "0.18.1" }', toml)
|
||||
lock = (d / "Cargo.lock").read_text()
|
||||
self.assertIn('name = "demo"\nversion = "0.21.0"', lock)
|
||||
self.assertIn('name = "demo-geom"\nversion = "0.21.0"', lock)
|
||||
self.assertIn('name = "serde"\nversion = "0.18.1"\nsource', lock)
|
||||
self.assertIn('name = "xtask"\nversion = "0.1.0"', lock)
|
||||
self.assertEqual(cut_release.workspace_version(d), "0.21.0")
|
||||
|
||||
def test_keeps_crlf_files_crlf(self):
|
||||
d = self.repo(crlf=True)
|
||||
cut_release.bump_cargo(d, "0.21.0")
|
||||
for name in ("Cargo.toml", "Cargo.lock"):
|
||||
raw = (d / name).read_bytes()
|
||||
self.assertNotIn(b"\n", raw.replace(b"\r\n", b""), name)
|
||||
|
||||
def test_refuses_a_bump_to_the_same_version_or_a_bad_one(self):
|
||||
d = self.repo()
|
||||
with self.assertRaises(ValueError):
|
||||
cut_release.bump_cargo(d, "0.18.1")
|
||||
with self.assertRaises(ValueError):
|
||||
cut_release.bump_cargo(d, "0.21")
|
||||
|
||||
def test_runs_the_repositorys_own_hook_with_old_and_new(self):
|
||||
d = self.repo()
|
||||
(d / "packaging").mkdir()
|
||||
(d / cut_release.HOOK).write_text("#!/bin/bash\n")
|
||||
calls = []
|
||||
old = cut_release.bump(d, "0.21.0", run=lambda cmd, **kw: calls.append((cmd, kw["cwd"])))
|
||||
self.assertEqual(old, "0.18.1")
|
||||
self.assertEqual(calls, [(["bash", cut_release.HOOK, "0.18.1", "0.21.0"], d)])
|
||||
|
||||
def test_a_comment_after_the_workspace_version_stays(self):
|
||||
d = self.repo(toml=CARGO_TOML.replace('version = "0.18.1"\nedition', 'version = "0.18.1" # the release\nedition'))
|
||||
cut_release.bump_cargo(d, "0.21.0")
|
||||
self.assertIn('version = "0.21.0" # the release', (d / "Cargo.toml").read_text())
|
||||
self.assertEqual(cut_release.workspace_version(d), "0.21.0")
|
||||
|
||||
def test_given_the_members_only_their_lock_entries_change(self):
|
||||
# A local path dependency outside the workspace keeps its version even when it matches.
|
||||
d = self.repo()
|
||||
_, _, packages = cut_release.bump_cargo(d, "0.21.0", members={"demo"})
|
||||
lock = (d / "Cargo.lock").read_text()
|
||||
self.assertEqual(packages, 1)
|
||||
self.assertIn('name = "demo"\nversion = "0.21.0"', lock)
|
||||
self.assertIn('name = "demo-geom"\nversion = "0.18.1"', lock)
|
||||
|
||||
def test_a_path_dependency_at_an_earlier_patch_of_the_old_minor_is_bumped_too(self):
|
||||
# pdfcraft at 0.21.1 named its crates at 0.21.0: that matched 0.21.1, and not 0.22.0.
|
||||
d = self.repo(toml=CARGO_TOML.replace('demo-io = { path = "crates/io", version = "0.18.1" }',
|
||||
'demo-io = { path = "crates/io", version = "0.18.0" }\n'
|
||||
'demo-old = { path = "crates/old", version = "0.17.0" }'))
|
||||
_, lines, _ = cut_release.bump_cargo(d, "0.19.0")
|
||||
toml = (d / "Cargo.toml").read_text()
|
||||
self.assertEqual(lines, 3)
|
||||
self.assertIn('demo-io = { path = "crates/io", version = "0.19.0" }', toml)
|
||||
self.assertIn('demo-old = { path = "crates/old", version = "0.17.0" }', toml)
|
||||
|
||||
def test_lock_dependency_lists_that_name_the_version_follow_the_bump(self):
|
||||
# vectorcraft also has its own crate from an older git revision in the graph, so Cargo
|
||||
# writes `"demo-geom 0.18.1"` in dependency lists.
|
||||
lock = CARGO_LOCK.replace(' "demo-geom",\n', ' "demo-geom 0.18.1",\n') + (
|
||||
'\n[[package]]\nname = "demo-geom"\nversion = "0.12.0"\nsource = "git+https://example.invalid/demo?rev=abc#abc"\n'
|
||||
'\n[[package]]\nname = "other"\nversion = "0.3.0"\ndependencies = [\n "demo-geom 0.12.0",\n]\n')
|
||||
d = self.repo(lock=lock)
|
||||
cut_release.bump_cargo(d, "0.21.0")
|
||||
lock = (d / "Cargo.lock").read_text()
|
||||
self.assertIn(' "demo-geom 0.21.0",\n', lock)
|
||||
self.assertIn(' "demo-geom 0.12.0",\n', lock)
|
||||
self.assertNotIn("0.18.1\",", lock)
|
||||
|
||||
def test_no_hook_no_call(self):
|
||||
d = self.repo()
|
||||
calls = []
|
||||
cut_release.bump(d, "0.21.0", run=lambda *a, **kw: calls.append(a))
|
||||
self.assertEqual(calls, [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user