From c76c94098e1495ee8e58b2538714834a4e05f483 Mon Sep 17 00:00:00 2001 From: Leynos Date: Sun, 21 Sep 2025 22:40:45 +0100 Subject: [PATCH 1/3] Address review feedback --- .../scripts/check_github_release.py | 2 +- .../scripts/confirm_release.py | 2 +- .../scripts/determine_release.py | 2 +- .../scripts/publish_release.py | 21 +++++++++++++------ .../scripts/validate_toml_versions.py | 2 +- .../scripts/write_summary.py | 2 +- .../tests/test_validate_toml_versions.py | 4 ++-- .../tests/test_cross_install.py | 8 ++++--- 8 files changed, 27 insertions(+), 16 deletions(-) diff --git a/.github/actions/release-to-pypi-uv/scripts/check_github_release.py b/.github/actions/release-to-pypi-uv/scripts/check_github_release.py index 7b3f0fbc..f0b29b5c 100644 --- a/.github/actions/release-to-pypi-uv/scripts/check_github_release.py +++ b/.github/actions/release-to-pypi-uv/scripts/check_github_release.py @@ -1,7 +1,7 @@ #!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.13" -# dependencies = ["typer"] +# dependencies = ["typer>=0.17,<0.18"] # /// """Verify that the GitHub Release for the provided tag exists and is published.""" diff --git a/.github/actions/release-to-pypi-uv/scripts/confirm_release.py b/.github/actions/release-to-pypi-uv/scripts/confirm_release.py index e1450a86..ae1ce08e 100644 --- a/.github/actions/release-to-pypi-uv/scripts/confirm_release.py +++ b/.github/actions/release-to-pypi-uv/scripts/confirm_release.py @@ -1,7 +1,7 @@ #!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.13" -# dependencies = ["typer"] +# dependencies = ["typer>=0.17,<0.18"] # /// """Validate that the caller supplied the expected confirmation string.""" diff --git a/.github/actions/release-to-pypi-uv/scripts/determine_release.py b/.github/actions/release-to-pypi-uv/scripts/determine_release.py index 54d9af5c..3e1276c9 100644 --- a/.github/actions/release-to-pypi-uv/scripts/determine_release.py +++ b/.github/actions/release-to-pypi-uv/scripts/determine_release.py @@ -1,7 +1,7 @@ #!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.13" -# dependencies = ["typer"] +# dependencies = ["typer>=0.17,<0.18"] # /// """Resolve the release tag and semantic version for the current run.""" diff --git a/.github/actions/release-to-pypi-uv/scripts/publish_release.py b/.github/actions/release-to-pypi-uv/scripts/publish_release.py index f9f7488b..8f66f40c 100644 --- a/.github/actions/release-to-pypi-uv/scripts/publish_release.py +++ b/.github/actions/release-to-pypi-uv/scripts/publish_release.py @@ -1,7 +1,7 @@ #!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.13" -# dependencies = ["typer"] +# dependencies = ["typer>=0.17,<0.18"] # /// """Publish the built distributions using uv.""" @@ -31,8 +31,6 @@ def _extend_sys_path() -> None: candidates.append(scripts_dir.parents[3]) for candidate in candidates: - if not candidate: - continue if not candidate.exists(): continue path_str = str(candidate) @@ -44,10 +42,14 @@ def _extend_sys_path() -> None: from cmd_utils import run_cmd # noqa: E402 -INDEX_OPTION = typer.Option("", envvar="INPUT_UV_INDEX") +INDEX_OPTION = typer.Option( + "", + envvar="INPUT_UV_INDEX", + help="Optional index name or URL for uv publish.", +) -def main(index: str = INDEX_OPTION) -> None: +def main(index: str = "") -> None: """Publish the built distributions with uv. Parameters @@ -55,6 +57,7 @@ def main(index: str = INDEX_OPTION) -> None: index : str Optional package index name or URL to pass to ``uv publish``. """ + index = index.strip() if index: typer.echo(f"Publishing with uv to index '{index}'") run_cmd(["uv", "publish", "--index", index]) @@ -63,5 +66,11 @@ def main(index: str = INDEX_OPTION) -> None: run_cmd(["uv", "publish"]) +def cli(index: str = INDEX_OPTION) -> None: + """CLI entrypoint.""" + + main(index=index) + + if __name__ == "__main__": - typer.run(main) + typer.run(cli) diff --git a/.github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py b/.github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py index 90b81cdb..5dfbaccb 100644 --- a/.github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py +++ b/.github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py @@ -1,7 +1,7 @@ #!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.13" -# dependencies = ["typer"] +# dependencies = ["typer>=0.17,<0.18"] # /// """Validate that project versions in pyproject.toml files match the release version.""" diff --git a/.github/actions/release-to-pypi-uv/scripts/write_summary.py b/.github/actions/release-to-pypi-uv/scripts/write_summary.py index 8e385557..a938e378 100644 --- a/.github/actions/release-to-pypi-uv/scripts/write_summary.py +++ b/.github/actions/release-to-pypi-uv/scripts/write_summary.py @@ -1,7 +1,7 @@ #!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.13" -# dependencies = ["typer"] +# dependencies = ["typer>=0.17,<0.18"] # /// """Append a short release summary for the workflow run.""" diff --git a/.github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py b/.github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py index f5a781b2..55f7d4dc 100644 --- a/.github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py +++ b/.github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py @@ -32,7 +32,7 @@ def _write_pyproject(base: Path, content: str) -> None: (base / "pyproject.toml").write_text(content.strip()) -def _invoke_main(module: ModuleType, **kwargs: str) -> None: +def _invoke_main(module: ModuleType, **kwargs: typ.Any) -> None: """Invoke ``module.main`` with defaults tailored for the tests.""" kwargs.setdefault("pattern", "**/pyproject.toml") kwargs.setdefault("fail_on_dynamic", "false") @@ -199,7 +199,7 @@ def test_dynamic_version_allowed_when_flag_unset( """, ) - _invoke_main(module, version="1.0.0", fail_on_dynamic="") + _invoke_main(module, version="1.0.0", fail_on_dynamic=None) captured = capsys.readouterr() assert "uses dynamic 'version'" in captured.out diff --git a/.github/actions/rust-build-release/tests/test_cross_install.py b/.github/actions/rust-build-release/tests/test_cross_install.py index 3b7fdfed..14a6f5d0 100644 --- a/.github/actions/rust-build-release/tests/test_cross_install.py +++ b/.github/actions/rust-build-release/tests/test_cross_install.py @@ -421,9 +421,11 @@ def fake_which(name: str) -> str | None: cmd_mox.verify() assert len(harness.calls) == 2 - assert "--git" in harness.calls[1] - assert "--tag" in harness.calls[1] - assert "v0.2.5" in harness.calls[1] + first, second = harness.calls + # First attempt was crates.io + assert "--git" not in first and "--tag" not in first + # Second attempt is the git fallback with a tag + assert "--git" in second and "--tag" in second and "v0.2.5" in second assert path == cross_path assert ver == "0.2.5" From addd5d35a816e0d83f6777224123786e549488a3 Mon Sep 17 00:00:00 2001 From: Leynos Date: Mon, 22 Sep 2025 00:24:17 +0100 Subject: [PATCH 2/3] Add CLI coverage for publish and validate scripts --- .../scripts/publish_release.py | 3 +- .../tests/test_publish_release.py | 73 ++++++++++++++++++- .../tests/test_validate_toml_versions.py | 23 ++++++ 3 files changed, 94 insertions(+), 5 deletions(-) diff --git a/.github/actions/release-to-pypi-uv/scripts/publish_release.py b/.github/actions/release-to-pypi-uv/scripts/publish_release.py index 8f66f40c..5262e223 100644 --- a/.github/actions/release-to-pypi-uv/scripts/publish_release.py +++ b/.github/actions/release-to-pypi-uv/scripts/publish_release.py @@ -57,8 +57,7 @@ def main(index: str = "") -> None: index : str Optional package index name or URL to pass to ``uv publish``. """ - index = index.strip() - if index: + if index := index.strip(): typer.echo(f"Publishing with uv to index '{index}'") run_cmd(["uv", "publish", "--index", index]) else: diff --git a/.github/actions/release-to-pypi-uv/tests/test_publish_release.py b/.github/actions/release-to-pypi-uv/tests/test_publish_release.py index 7b705e3c..633b8e3e 100644 --- a/.github/actions/release-to-pypi-uv/tests/test_publish_release.py +++ b/.github/actions/release-to-pypi-uv/tests/test_publish_release.py @@ -8,6 +8,7 @@ from types import ModuleType import pytest +from typer.testing import CliRunner from ._helpers import REPO_ROOT, load_script_module @@ -22,7 +23,9 @@ def fixture_publish_module() -> ModuleType: def test_publish_default_index( - monkeypatch: pytest.MonkeyPatch, publish_module: ModuleType + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], + publish_module: ModuleType, ) -> None: """Invoke ``uv publish`` without an index when none is provided.""" calls: list[list[str]] = [] @@ -35,10 +38,14 @@ def fake_run_cmd(args: list[str], **_: object) -> None: publish_module.main(index="") assert calls == [["uv", "publish"]] + captured = capsys.readouterr() + assert "Publishing with uv to default index (PyPI)" in captured.out def test_publish_custom_index( - monkeypatch: pytest.MonkeyPatch, publish_module: ModuleType + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], + publish_module: ModuleType, ) -> None: """Add the ``--index`` flag when a custom index value is supplied.""" calls: list[list[str]] = [] @@ -48,9 +55,11 @@ def fake_run_cmd(args: list[str], **_: object) -> None: monkeypatch.setattr(publish_module, "run_cmd", fake_run_cmd) - publish_module.main(index="testpypi") + publish_module.main(index=" testpypi ") assert calls == [["uv", "publish", "--index", "testpypi"]] + captured = capsys.readouterr() + assert "Publishing with uv to index 'testpypi'" in captured.out def test_publish_run_cmd_error( @@ -69,3 +78,61 @@ def fake_run_cmd(_: list[str], **__: object) -> None: with pytest.raises(DummyError): publish_module.main(index="") + + +def test_cli_proxies_to_main( + monkeypatch: pytest.MonkeyPatch, publish_module: ModuleType +) -> None: + """Ensure the CLI entrypoint forwards arguments to ``main``.""" + received: dict[str, str] = {} + + def fake_main(*, index: str) -> None: + received["index"] = index + + monkeypatch.setattr(publish_module, "main", fake_main) + + publish_module.cli(index="mirror") + + assert received == {"index": "mirror"} + + +def test_cli_runner_default_index( + monkeypatch: pytest.MonkeyPatch, publish_module: ModuleType +) -> None: + """Exercise the CLI behaviour when no index is provided.""" + calls: list[list[str]] = [] + + def fake_run_cmd(args: list[str], **_: object) -> None: + calls.append(args) + + monkeypatch.setattr(publish_module, "run_cmd", fake_run_cmd) + + runner = CliRunner() + app = publish_module.typer.Typer() + app.command()(publish_module.cli) + result = runner.invoke(app, []) + + assert result.exit_code == 0 + assert calls == [["uv", "publish"]] + assert "Publishing with uv to default index (PyPI)" in result.output + + +def test_cli_runner_respects_env_index( + monkeypatch: pytest.MonkeyPatch, publish_module: ModuleType +) -> None: + """Accept the index from the GitHub Action input environment variable.""" + calls: list[list[str]] = [] + + def fake_run_cmd(args: list[str], **_: object) -> None: + calls.append(args) + + monkeypatch.setattr(publish_module, "run_cmd", fake_run_cmd) + + runner = CliRunner() + app = publish_module.typer.Typer() + app.command()(publish_module.cli) + result = runner.invoke(app, [], env={"INPUT_UV_INDEX": "testpypi"}) + + assert result.exit_code == 0 + assert calls == [["uv", "publish", "--index", "testpypi"]] + assert "Publishing with uv to index 'testpypi'" in result.output diff --git a/.github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py b/.github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py index 55f7d4dc..38dadd0a 100644 --- a/.github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py +++ b/.github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py @@ -9,6 +9,7 @@ from types import ModuleType import pytest +from typer.testing import CliRunner from ._helpers import load_script_module @@ -61,6 +62,28 @@ def test_passes_when_versions_match( ) +def test_cli_defaults_when_optional_parameters_omitted( + project_root: Path, module: ModuleType +) -> None: + """Use default CLI values when optional flags are not provided.""" + _write_pyproject( + project_root / "pkg", + """ +[project] +name = "demo" +version = "1.0.0" +""", + ) + + runner = CliRunner() + app = module.typer.Typer() + app.command()(module.main) + result = runner.invoke(app, ["--version", "1.0.0"]) + + assert result.exit_code == 0 + assert "all versions match 1.0.0" in result.output + + def test_fails_on_mismatch( project_root: Path, module: ModuleType, capsys: pytest.CaptureFixture[str] ) -> None: From 8817eeb0c775a904937ad238e389e79d8d3ad58c Mon Sep 17 00:00:00 2001 From: Leynos Date: Mon, 22 Sep 2025 07:53:09 +0100 Subject: [PATCH 3/3] Add deterministic validation tests and bunx validator checks --- .../scripts/publish_release.py | 1 - .../tests/test_validate_toml_versions.py | 24 +++++++++- .../tests/test_cross_install.py | 45 ++++++++++++++++++- Makefile | 2 +- 4 files changed, 66 insertions(+), 6 deletions(-) diff --git a/.github/actions/release-to-pypi-uv/scripts/publish_release.py b/.github/actions/release-to-pypi-uv/scripts/publish_release.py index 5262e223..e5616cba 100644 --- a/.github/actions/release-to-pypi-uv/scripts/publish_release.py +++ b/.github/actions/release-to-pypi-uv/scripts/publish_release.py @@ -67,7 +67,6 @@ def main(index: str = "") -> None: def cli(index: str = INDEX_OPTION) -> None: """CLI entrypoint.""" - main(index=index) diff --git a/.github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py b/.github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py index 38dadd0a..7c36993f 100644 --- a/.github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py +++ b/.github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py @@ -33,7 +33,7 @@ def _write_pyproject(base: Path, content: str) -> None: (base / "pyproject.toml").write_text(content.strip()) -def _invoke_main(module: ModuleType, **kwargs: typ.Any) -> None: +def _invoke_main(module: ModuleType, **kwargs: object) -> None: """Invoke ``module.main`` with defaults tailored for the tests.""" kwargs.setdefault("pattern", "**/pyproject.toml") kwargs.setdefault("fail_on_dynamic", "false") @@ -222,7 +222,7 @@ def test_dynamic_version_allowed_when_flag_unset( """, ) - _invoke_main(module, version="1.0.0", fail_on_dynamic=None) + _invoke_main(module, version="1.0.0") captured = capsys.readouterr() assert "uses dynamic 'version'" in captured.out @@ -320,6 +320,26 @@ def fake_glob( assert discovered == [first, second] +def test_iter_files_discovers_paths_in_sorted_order( + project_root: Path, + module: ModuleType, +) -> None: + """Ensure discovery order remains deterministic for reproducible output.""" + for name in ("pkg_c", "pkg_a", "pkg_b"): + _write_pyproject( + project_root / name, + """ +[project] +name = "demo" +version = "1.0.0" +""", + ) + + discovered = list(module._iter_files("**/pyproject.toml")) + relative = [path.as_posix() for path in discovered] + assert relative == sorted(relative) + + @pytest.mark.parametrize("value", ["true", "TRUE", "Yes", "1", "on"]) def test_parse_bool_truthy_values(module: ModuleType, value: str) -> None: """Treat recognised truthy values as ``True`` for configuration flags.""" diff --git a/.github/actions/rust-build-release/tests/test_cross_install.py b/.github/actions/rust-build-release/tests/test_cross_install.py index 14a6f5d0..aafbcaa5 100644 --- a/.github/actions/rust-build-release/tests/test_cross_install.py +++ b/.github/actions/rust-build-release/tests/test_cross_install.py @@ -13,6 +13,7 @@ CMD_MOX_UNSUPPORTED, _register_cross_version_stub, _register_docker_info_stub, + _register_podman_info_stub, _register_rustup_toolchain_stub, ) @@ -423,9 +424,12 @@ def fake_which(name: str) -> str | None: assert len(harness.calls) == 2 first, second = harness.calls # First attempt was crates.io - assert "--git" not in first and "--tag" not in first + assert "--git" not in first + assert "--tag" not in first # Second attempt is the git fallback with a tag - assert "--git" in second and "--tag" in second and "v0.2.5" in second + assert "--git" in second + assert "--tag" in second + assert "v0.2.5" in second assert path == cross_path assert ver == "0.2.5" @@ -465,6 +469,43 @@ def fake_which(name: str) -> str | None: assert all(cmd[0] != "cross" for cmd in app_env.calls) +@CMD_MOX_UNSUPPORTED +def test_falls_back_to_cargo_when_podman_unusable( + main_module: ModuleType, + cross_module: ModuleType, + module_harness: HarnessFactory, + cmd_mox: CmdMox, +) -> None: + """Falls back to cargo when podman exists but is unusable.""" + cross_env = module_harness(cross_module) + app_env = module_harness(main_module) + + default_toolchain = main_module.DEFAULT_TOOLCHAIN + rustup_stdout = f"{default_toolchain}-x86_64-unknown-linux-gnu\n" + cross_path = _register_cross_version_stub(cmd_mox) + rustup_path = _register_rustup_toolchain_stub(cmd_mox, rustup_stdout) + podman_path = _register_podman_info_stub(cmd_mox, exit_code=1) + + def fake_which(name: str) -> str | None: + if name == "podman": + return podman_path + if name == "cross": + return cross_path + if name == "rustup": + return rustup_path + return None + + cross_env.patch_shutil_which(fake_which) + app_env.patch_shutil_which(fake_which) + + cmd_mox.replay() + main_module.main("x86_64-unknown-linux-gnu", default_toolchain) + cmd_mox.verify() + + assert any(cmd[0] == "cargo" for cmd in app_env.calls) + assert all(cmd[0] != "cross" for cmd in app_env.calls) + + def test_returns_none_when_install_fails_on_windows( cross_module: ModuleType, module_harness: HarnessFactory, diff --git a/Makefile b/Makefile index 376b3e2f..206e0766 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ test: .venv ## Run tests lint: ## Check test scripts and actions uvx ruff check find .github/actions -type f \( -name 'action.yml' -o -name 'action.yaml' \) -print0 \ - | xargs -r -0 -n1 ${HOME}/.bun/bin/action-validator + | xargs -r -0 -n1 bunx -y @action-validator/cli typecheck: .venv ## Run static type checking with Ty ./.venv/bin/ty check \