Skip to content

Commit ae01f3d

Browse files
authored
Add crate name output to ensure-cargo-version action (#154)
* Add crate name output to ensure-cargo-version action * Add coverage for crate name validation * Fix lint issues in ensure-cargo-version tests
1 parent c0a9072 commit ae01f3d

File tree

4 files changed

+102
-9
lines changed

4 files changed

+102
-9
lines changed

.github/actions/ensure-cargo-version/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Validate that the Git tag triggering a release workflow matches the version in o
1616
| ---- | ----------- |
1717
| `version` | Version extracted from the tag reference after removing the configured prefix. |
1818
| `crate-version` | Version read from the first manifest path provided (after resolution) after resolving workspace inheritance. |
19+
| `crate-name` | Package name read from the first manifest path provided (after resolution). |
1920

2021
## Usage
2122

.github/actions/ensure-cargo-version/action.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,6 @@ outputs:
4646
crate-version:
4747
description: Version number declared in the first Cargo manifest.
4848
value: ${{ steps.verify.outputs['crate-version'] }}
49+
crate-name:
50+
description: Package name declared in the first Cargo manifest.
51+
value: ${{ steps.verify.outputs['crate-name'] }}

.github/actions/ensure-cargo-version/scripts/ensure_cargo_version.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,10 @@
2727

2828
@dataclasses.dataclass(slots=True)
2929
class ManifestVersion:
30-
"""Version metadata extracted from a manifest."""
30+
"""Metadata extracted from a manifest."""
3131

3232
path: Path
33+
name: str
3334
version: str
3435

3536

@@ -59,7 +60,7 @@ def _resolve_paths(manifests: list[Path]) -> list[Path]:
5960

6061

6162
def _read_manifest_version(path: Path) -> ManifestVersion:
62-
"""Parse a manifest and return the discovered package version."""
63+
"""Parse a manifest and return the discovered package metadata."""
6364
try:
6465
with path.open("rb") as handle:
6566
data = tomllib.load(handle)
@@ -72,6 +73,11 @@ def _read_manifest_version(path: Path) -> ManifestVersion:
7273
if not isinstance(package, dict):
7374
raise ManifestError(path, "Manifest missing [package] table")
7475

76+
name = package.get("name")
77+
if not isinstance(name, str) or not name.strip():
78+
raise ManifestError(path, "Could not read package.name")
79+
crate_name = name.strip()
80+
7581
version = package.get("version")
7682
if isinstance(version, dict) and version.get("workspace") is True:
7783
workspace_manifest = _find_workspace_root(path.parent)
@@ -86,12 +92,12 @@ def _read_manifest_version(path: Path) -> ManifestVersion:
8692
workspace_manifest,
8793
"Workspace manifest missing [workspace.package].version",
8894
)
89-
return ManifestVersion(path=path, version=workspace_version)
95+
return ManifestVersion(path=path, name=crate_name, version=workspace_version)
9096

9197
if not isinstance(version, str) or not version.strip():
9298
raise ManifestError(path, "Could not read package.version")
9399

94-
return ManifestVersion(path=path, version=version.strip())
100+
return ManifestVersion(path=path, name=crate_name, version=version.strip())
95101

96102

97103
def _find_workspace_root(start_dir: Path) -> Path | None:
@@ -244,6 +250,7 @@ def main(
244250
)
245251

246252
crate_version = manifest_versions[0].version if manifest_versions else ""
253+
crate_name = manifest_versions[0].name if manifest_versions else ""
247254

248255
if should_check_tag:
249256
if tag_version is None: # pragma: no cover - defensive guard
@@ -271,6 +278,7 @@ def main(
271278
raise SystemExit(1)
272279

273280
_write_output("crate-version", crate_version)
281+
_write_output("crate-name", crate_name)
274282
manifest_list = ", ".join(_display_path(item.path) for item in manifest_versions)
275283
if should_check_tag:
276284
print(

.github/actions/ensure-cargo-version/scripts/tests/test_ensure_cargo_version.py

Lines changed: 86 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,15 +61,53 @@ def test_coerce_bool_rejects_invalid_values() -> None:
6161
ensure._coerce_bool(value="not-a-boolean", parameter="check-tag")
6262

6363

64-
def _write_manifest(path: Path, version: str) -> None:
65-
"""Write a simple manifest declaring ``version`` to ``path``."""
64+
def _write_manifest(path: Path, version: str, *, name: str = "demo") -> None:
65+
"""Write a simple manifest declaring ``name`` and ``version`` to ``path``."""
6666
path.parent.mkdir(parents=True, exist_ok=True)
6767
path.write_text(
68-
f"""[package]\nname = \"demo\"\nversion = \"{version}\"\n""",
68+
f"""[package]\nname = \"{name}\"\nversion = \"{version}\"\n""",
6969
encoding="utf-8",
7070
)
7171

7272

73+
def _write_raw_manifest(path: Path, contents: str) -> None:
74+
"""Write a manifest with ``contents`` directly to ``path``."""
75+
path.parent.mkdir(parents=True, exist_ok=True)
76+
path.write_text(contents, encoding="utf-8")
77+
78+
79+
@pytest.mark.parametrize(
80+
"manifest_contents",
81+
[
82+
'[package]\nversion = "1.2.3"\n',
83+
'[package]\nname = ""\nversion = "1.2.3"\n',
84+
'[package]\nname = " "\nversion = "1.2.3"\n',
85+
],
86+
)
87+
def test_read_manifest_version_rejects_invalid_names(
88+
tmp_path: Path, manifest_contents: str
89+
) -> None:
90+
"""A manifest must declare a non-empty ``package.name``."""
91+
manifest_path = tmp_path / "Cargo.toml"
92+
_write_raw_manifest(manifest_path, manifest_contents)
93+
94+
with pytest.raises(ensure.ManifestError, match=r"package\.name"):
95+
ensure._read_manifest_version(manifest_path)
96+
97+
98+
def test_read_manifest_version_trims_whitespace_from_name(tmp_path: Path) -> None:
99+
"""Whitespace around the crate name should be ignored."""
100+
manifest_path = tmp_path / "Cargo.toml"
101+
_write_raw_manifest(
102+
manifest_path,
103+
'[package]\nname = " demo-crate "\nversion = "1.2.3"\n',
104+
)
105+
106+
manifest_version = ensure._read_manifest_version(manifest_path)
107+
108+
assert manifest_version.name == "demo-crate"
109+
110+
73111
def test_main_skips_tag_comparison_when_disabled(
74112
monkeypatch: pytest.MonkeyPatch,
75113
tmp_path: Path,
@@ -89,6 +127,7 @@ def test_main_skips_tag_comparison_when_disabled(
89127

90128
contents = output_file.read_text(encoding="utf-8").splitlines()
91129
assert "crate-version=1.2.4" in contents
130+
assert "crate-name=demo" in contents
92131
assert "version=9.9.9" in contents
93132

94133
captured = capsys.readouterr()
@@ -114,6 +153,7 @@ def test_main_with_disabled_tag_check_does_not_require_ref(
114153

115154
contents = output_file.read_text(encoding="utf-8").splitlines()
116155
assert "crate-version=7.8.9" in contents
156+
assert "crate-name=demo" in contents
117157
assert not any(line.startswith("version=") for line in contents)
118158

119159
captured = capsys.readouterr()
@@ -160,8 +200,8 @@ def test_main_records_first_manifest_version_in_output(
160200
first_manifest = workspace / "Cargo.toml"
161201
second_manifest = workspace / "crates" / "other" / "Cargo.toml"
162202

163-
_write_manifest(first_manifest, "3.4.5")
164-
_write_manifest(second_manifest, "9.9.9")
203+
_write_manifest(first_manifest, "3.4.5", name="primary")
204+
_write_manifest(second_manifest, "9.9.9", name="secondary")
165205

166206
output_file = workspace / "outputs"
167207
monkeypatch.setenv("GITHUB_OUTPUT", str(output_file))
@@ -175,6 +215,7 @@ def test_main_records_first_manifest_version_in_output(
175215

176216
contents = output_file.read_text(encoding="utf-8").splitlines()
177217
assert "crate-version=3.4.5" in contents
218+
assert "crate-name=primary" in contents
178219
assert "version=3.4.5" in contents
179220

180221
captured = capsys.readouterr()
@@ -200,7 +241,47 @@ def test_main_emits_crate_version_when_checking_tag(
200241

201242
contents = output_file.read_text(encoding="utf-8").splitlines()
202243
assert "crate-version=4.5.6" in contents
244+
assert "crate-name=demo" in contents
203245
assert "version=4.5.6" in contents
204246

205247
captured = capsys.readouterr()
206248
assert "Release tag 4.5.6 matches" in captured.out
249+
250+
251+
@pytest.mark.parametrize(
252+
"manifest_contents",
253+
[
254+
'[package]\nversion = "1.2.3"\n',
255+
'[package]\nname = ""\nversion = "1.2.3"\n',
256+
'[package]\nname = " "\nversion = "1.2.3"\n',
257+
],
258+
)
259+
def test_main_aborts_when_crate_name_missing_or_blank(
260+
monkeypatch: pytest.MonkeyPatch,
261+
tmp_path: Path,
262+
manifest_contents: str,
263+
) -> None:
264+
"""Invalid crate names should terminate the run with an error."""
265+
workspace = tmp_path
266+
manifest_path = workspace / "Cargo.toml"
267+
_write_raw_manifest(manifest_path, manifest_contents)
268+
269+
output_file = workspace / "outputs"
270+
monkeypatch.setenv("GITHUB_OUTPUT", str(output_file))
271+
monkeypatch.setenv("GITHUB_WORKSPACE", str(workspace))
272+
273+
recorded_errors: list[tuple[str, str, Path | None]] = []
274+
275+
def record_error(title: str, message: str, *, path: Path | None = None) -> None:
276+
recorded_errors.append((title, message, path))
277+
278+
monkeypatch.setattr(ensure, "_emit_error", record_error)
279+
280+
with pytest.raises(SystemExit) as excinfo:
281+
ensure.main(manifests=[Path("Cargo.toml")], check_tag="false")
282+
283+
assert excinfo.value.code == 1
284+
assert recorded_errors
285+
error_titles = {title for title, _, _ in recorded_errors}
286+
assert "Cargo.toml parse failure" in error_titles
287+
assert any("package.name" in message for _, message, _ in recorded_errors)

0 commit comments

Comments
 (0)