Public Access
DEVX-118: feat: enhance documentation-as-code with badges, version refs, Vale
Post-merge / detect-type (push) Successful in 11s
Post-merge / validate-commit-msg (push) Successful in 13s
Post-merge / vikunja (push) Successful in 19s
Post-merge / configure-repo (push) Successful in 15s
Post-merge / release (push) Successful in 45s
Post-merge / sync-wiki (push) Successful in 50s
Post-merge / publish (push) Successful in 32s
Post-merge / badges (push) Failing after 36s
Post-merge / detect-type (push) Successful in 11s
Post-merge / validate-commit-msg (push) Successful in 13s
Post-merge / vikunja (push) Successful in 19s
Post-merge / configure-repo (push) Successful in 15s
Post-merge / release (push) Successful in 45s
Post-merge / sync-wiki (push) Successful in 50s
Post-merge / publish (push) Successful in 32s
Post-merge / badges (push) Failing after 36s
- Fix badge system: clean .badges dir from orphan branch, add version verification, make badges job depend on release (avoids stale version badge race condition) - Add check_doc_versions.py: lint tool that verifies docs version references match current __version__, with --fix for auto-update - Integrate check_doc_versions into release process (auto-updates docs on every release commit) - Add Vale prose linter integration: .vale.ini, custom styles for terminology and code block language, CI step, make target - Fix stale version references in docs (0.27.0 → 0.33.4) - Fix e.g. → for example in docs (Google.Latin Vale rule) - Add CI steps for check_doc_versions and Vale to quality workflow - Add make targets: devx-check-doc-versions, devx-vale Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
3e12cf222f
commit
bbf0c81c32
@@ -0,0 +1,255 @@
|
||||
"""Tests for devx.tools.check_doc_versions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from click.testing import CliRunner
|
||||
|
||||
import devx.tools.check_doc_versions as cdv
|
||||
|
||||
|
||||
class TestDetectPackageName:
|
||||
def test_finds_package(self, tmp_path: Path) -> None:
|
||||
src = tmp_path / "src" / "myproj"
|
||||
src.mkdir(parents=True)
|
||||
(src / "__init__.py").write_text('__version__ = "1.0.0"\n')
|
||||
assert cdv.detect_package_name(tmp_path) == "myproj"
|
||||
|
||||
def test_no_src_dir(self, tmp_path: Path) -> None:
|
||||
assert cdv.detect_package_name(tmp_path) is None
|
||||
|
||||
def test_no_init_py(self, tmp_path: Path) -> None:
|
||||
src = tmp_path / "src" / "myproj"
|
||||
src.mkdir(parents=True)
|
||||
assert cdv.detect_package_name(tmp_path) is None
|
||||
|
||||
|
||||
class TestReadVersion:
|
||||
def test_reads_version(self, tmp_path: Path) -> None:
|
||||
src = tmp_path / "src" / "myproj"
|
||||
src.mkdir(parents=True)
|
||||
(src / "__init__.py").write_text('__version__ = "2.3.4"\n')
|
||||
assert cdv.read_version(tmp_path, "myproj") == "2.3.4"
|
||||
|
||||
def test_no_version(self, tmp_path: Path) -> None:
|
||||
src = tmp_path / "src" / "myproj"
|
||||
src.mkdir(parents=True)
|
||||
(src / "__init__.py").write_text("# no version here\n")
|
||||
assert cdv.read_version(tmp_path, "myproj") is None
|
||||
|
||||
def test_no_init_file(self, tmp_path: Path) -> None:
|
||||
assert cdv.read_version(tmp_path, "nonexistent") is None
|
||||
|
||||
|
||||
class TestFindVersionRefs:
|
||||
def test_finds_gte_ref(self) -> None:
|
||||
content = ' "devx>=0.27.0",\n'
|
||||
refs = cdv.find_version_refs(content, "devx")
|
||||
assert len(refs) == 1
|
||||
_, full, op, ver, _ = refs[0]
|
||||
assert op == ">="
|
||||
assert ver == "0.27.0"
|
||||
|
||||
def test_finds_eq_ref(self) -> None:
|
||||
content = '"devx==0.33.4"'
|
||||
refs = cdv.find_version_refs(content, "devx")
|
||||
assert len(refs) == 1
|
||||
_, _, op, ver, _ = refs[0]
|
||||
assert op == "=="
|
||||
assert ver == "0.33.4"
|
||||
|
||||
def test_finds_extras_ref(self) -> None:
|
||||
content = '"devx[dev]>=0.27.0"'
|
||||
refs = cdv.find_version_refs(content, "devx")
|
||||
assert len(refs) == 1
|
||||
_, _, op, ver, _ = refs[0]
|
||||
assert op == ">="
|
||||
assert ver == "0.27.0"
|
||||
|
||||
def test_finds_upper_bound(self) -> None:
|
||||
content = '"devx>=0.27.0,<0.28"'
|
||||
refs = cdv.find_version_refs(content, "devx")
|
||||
assert len(refs) == 1
|
||||
_, _, _, _, rest = refs[0]
|
||||
assert "<0.28" in rest
|
||||
|
||||
def test_ignores_other_packages(self) -> None:
|
||||
content = '"other-pkg>=1.0.0"'
|
||||
refs = cdv.find_version_refs(content, "devx")
|
||||
assert len(refs) == 0
|
||||
|
||||
def test_multiple_refs(self) -> None:
|
||||
content = '"devx>=0.27.0"\n"devx==0.33.4"\n'
|
||||
refs = cdv.find_version_refs(content, "devx")
|
||||
assert len(refs) == 2
|
||||
|
||||
|
||||
class TestFixVersionRefs:
|
||||
def test_fixes_stale_version(self) -> None:
|
||||
content = '"devx>=0.27.0"'
|
||||
new, fixes = cdv.fix_version_refs(content, "devx", "0.33.4")
|
||||
assert fixes == 1
|
||||
assert "0.33.4" in new
|
||||
assert "0.27.0" not in new
|
||||
|
||||
def test_no_fix_needed(self) -> None:
|
||||
content = '"devx>=0.33.4"'
|
||||
new, fixes = cdv.fix_version_refs(content, "devx", "0.33.4")
|
||||
assert fixes == 0
|
||||
assert new == content
|
||||
|
||||
def test_fixes_upper_bound(self) -> None:
|
||||
content = '"devx>=0.27.0,<0.28"'
|
||||
new, fixes = cdv.fix_version_refs(content, "devx", "0.33.4")
|
||||
assert fixes == 1
|
||||
assert "0.33.4" in new
|
||||
assert "<0.34" in new
|
||||
assert "<0.28" not in new
|
||||
|
||||
def test_ignores_other_packages(self) -> None:
|
||||
content = '"other>=1.0.0"'
|
||||
new, fixes = cdv.fix_version_refs(content, "devx", "0.33.4")
|
||||
assert fixes == 0
|
||||
assert new == content
|
||||
|
||||
|
||||
class TestMain:
|
||||
def test_pass_when_current(self, tmp_path: Path) -> None:
|
||||
src = tmp_path / "src" / "devx"
|
||||
src.mkdir(parents=True)
|
||||
(src / "__init__.py").write_text('__version__ = "0.33.4"\n')
|
||||
readme = tmp_path / "README.md"
|
||||
readme.write_text('"devx>=0.33.4"\n')
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cdv.main, ["--root", str(tmp_path)])
|
||||
assert result.exit_code == 0
|
||||
assert "PASS" in result.output
|
||||
|
||||
def test_fail_when_stale(self, tmp_path: Path) -> None:
|
||||
src = tmp_path / "src" / "devx"
|
||||
src.mkdir(parents=True)
|
||||
(src / "__init__.py").write_text('__version__ = "0.33.4"\n')
|
||||
readme = tmp_path / "README.md"
|
||||
readme.write_text('"devx>=0.27.0"\n')
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cdv.main, ["--root", str(tmp_path)])
|
||||
assert result.exit_code == 1
|
||||
assert "stale" in result.output
|
||||
|
||||
def test_fix_updates_files(self, tmp_path: Path) -> None:
|
||||
src = tmp_path / "src" / "devx"
|
||||
src.mkdir(parents=True)
|
||||
(src / "__init__.py").write_text('__version__ = "0.33.4"\n')
|
||||
readme = tmp_path / "README.md"
|
||||
readme.write_text('"devx>=0.27.0"\n')
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cdv.main, ["--root", str(tmp_path), "--fix"])
|
||||
assert result.exit_code == 0
|
||||
assert "0.33.4" in readme.read_text()
|
||||
|
||||
def test_no_package_skips(self, tmp_path: Path) -> None:
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cdv.main, ["--root", str(tmp_path)])
|
||||
assert result.exit_code == 0
|
||||
assert "skipping" in result.output
|
||||
|
||||
def test_no_version_skips(self, tmp_path: Path) -> None:
|
||||
src = tmp_path / "src" / "devx"
|
||||
src.mkdir(parents=True)
|
||||
(src / "__init__.py").write_text("# no version\n")
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cdv.main, ["--root", str(tmp_path)])
|
||||
assert result.exit_code == 0
|
||||
assert "Cannot read" in result.output
|
||||
|
||||
def test_docs_only_skips_readme(self, tmp_path: Path) -> None:
|
||||
src = tmp_path / "src" / "devx"
|
||||
src.mkdir(parents=True)
|
||||
(src / "__init__.py").write_text('__version__ = "0.33.4"\n')
|
||||
readme = tmp_path / "README.md"
|
||||
readme.write_text('"devx>=0.27.0"\n')
|
||||
docs = tmp_path / "docs"
|
||||
docs.mkdir()
|
||||
(docs / "index.md").write_text('"devx>=0.33.4"\n')
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cdv.main, ["--root", str(tmp_path), "--docs-only"])
|
||||
assert result.exit_code == 0
|
||||
assert "PASS" in result.output
|
||||
|
||||
def test_fix_no_stale(self, tmp_path: Path) -> None:
|
||||
src = tmp_path / "src" / "devx"
|
||||
src.mkdir(parents=True)
|
||||
(src / "__init__.py").write_text('__version__ = "0.33.4"\n')
|
||||
readme = tmp_path / "README.md"
|
||||
readme.write_text('"devx>=0.33.4"\n')
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cdv.main, ["--root", str(tmp_path), "--fix"])
|
||||
assert result.exit_code == 0
|
||||
assert "No stale" in result.output
|
||||
|
||||
def test_checks_docs_dir(self, tmp_path: Path) -> None:
|
||||
src = tmp_path / "src" / "devx"
|
||||
src.mkdir(parents=True)
|
||||
(src / "__init__.py").write_text('__version__ = "0.33.4"\n')
|
||||
docs = tmp_path / "docs"
|
||||
docs.mkdir()
|
||||
(docs / "index.md").write_text('"devx>=0.27.0"\n')
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cdv.main, ["--root", str(tmp_path)])
|
||||
assert result.exit_code == 1
|
||||
assert "docs/index.md" in result.output
|
||||
|
||||
def test_detect_package_with_non_dir_entry(self, tmp_path: Path) -> None:
|
||||
src = tmp_path / "src"
|
||||
src.mkdir(parents=True)
|
||||
# `aaa_file.py` sorts before `devx/` so the non-dir branch is hit
|
||||
(src / "aaa_file.py").touch()
|
||||
pkg_dir = src / "devx"
|
||||
pkg_dir.mkdir()
|
||||
(pkg_dir / "__init__.py").write_text('__version__ = "1.0.0"\n')
|
||||
assert cdv.detect_package_name(tmp_path) == "devx"
|
||||
|
||||
def test_read_version_auto_detect(self, tmp_path: Path) -> None:
|
||||
src = tmp_path / "src" / "devx"
|
||||
src.mkdir(parents=True)
|
||||
(src / "__init__.py").write_text('__version__ = "3.2.1"\n')
|
||||
assert cdv.read_version(tmp_path) == "3.2.1"
|
||||
|
||||
def test_read_version_no_package(self, tmp_path: Path) -> None:
|
||||
assert cdv.read_version(tmp_path) is None
|
||||
|
||||
def test_main_with_file_without_refs(self, tmp_path: Path) -> None:
|
||||
src = tmp_path / "src" / "devx"
|
||||
src.mkdir(parents=True)
|
||||
(src / "__init__.py").write_text('__version__ = "0.33.4"\n')
|
||||
docs = tmp_path / "docs"
|
||||
docs.mkdir()
|
||||
(docs / "index.md").write_text("# No version refs here\n")
|
||||
(docs / "other.md").write_text('"devx>=0.27.0"\n')
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cdv.main, ["--root", str(tmp_path)])
|
||||
assert result.exit_code == 1
|
||||
assert "other.md" in result.output
|
||||
|
||||
def test_fix_with_file_without_refs(self, tmp_path: Path) -> None:
|
||||
src = tmp_path / "src" / "devx"
|
||||
src.mkdir(parents=True)
|
||||
(src / "__init__.py").write_text('__version__ = "0.33.4"\n')
|
||||
readme = tmp_path / "README.md"
|
||||
readme.write_text("# No refs\n")
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cdv.main, ["--root", str(tmp_path), "--fix"])
|
||||
assert result.exit_code == 0
|
||||
assert "No stale" in result.output
|
||||
|
||||
def test_fix_with_current_refs(self, tmp_path: Path) -> None:
|
||||
src = tmp_path / "src" / "devx"
|
||||
src.mkdir(parents=True)
|
||||
(src / "__init__.py").write_text('__version__ = "0.33.4"\n')
|
||||
readme = tmp_path / "README.md"
|
||||
readme.write_text('"devx>=0.33.4"\n')
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cdv.main, ["--root", str(tmp_path), "--fix"])
|
||||
assert result.exit_code == 0
|
||||
assert "No stale" in result.output
|
||||
@@ -269,6 +269,34 @@ class TestInstallTofu:
|
||||
assert (tmp_path / "tofu").exists()
|
||||
|
||||
|
||||
class TestInstallVale:
|
||||
def test_already_installed(self) -> None:
|
||||
with patch.object(install_tools, "_is_installed", return_value=True):
|
||||
assert install_tools.install_vale() is True
|
||||
|
||||
def test_install(self, tmp_path: Path) -> None:
|
||||
import io
|
||||
import tarfile
|
||||
|
||||
tarball_path = tmp_path / "archive.tar.gz"
|
||||
binary_content = b"fake vale"
|
||||
with tarfile.open(tarball_path, "w:gz") as tar:
|
||||
info = tarfile.TarInfo(name="vale")
|
||||
info.size = len(binary_content)
|
||||
tar.addfile(info, io.BytesIO(binary_content))
|
||||
|
||||
with patch.object(install_tools, "_is_installed", return_value=False):
|
||||
with patch.object(install_tools, "TARGET_DIR", tmp_path):
|
||||
with patch.object(platform, "machine", return_value="x86_64"):
|
||||
with patch.object(
|
||||
install_tools,
|
||||
"_download",
|
||||
side_effect=lambda url, dest: Path(dest).write_bytes(tarball_path.read_bytes()),
|
||||
):
|
||||
assert install_tools.install_vale() is True
|
||||
assert (tmp_path / "vale").exists()
|
||||
|
||||
|
||||
class TestListTools:
|
||||
def test_list(self, tmp_path: Path) -> None:
|
||||
with patch.object(install_tools, "TARGET_DIR", tmp_path):
|
||||
@@ -308,6 +336,11 @@ class TestInstallTool:
|
||||
assert install_tools._install_tool("tofu") is True
|
||||
mock.assert_called_once()
|
||||
|
||||
def test_vale(self) -> None:
|
||||
with patch.object(install_tools, "install_vale", return_value=True) as mock:
|
||||
assert install_tools._install_tool("vale") is True
|
||||
mock.assert_called_once()
|
||||
|
||||
def test_unknown_tool(self) -> None:
|
||||
with pytest.raises(ClickException, match="Unknown tool"):
|
||||
install_tools._install_tool("unknown")
|
||||
@@ -326,7 +359,7 @@ class TestMain:
|
||||
with patch.object(install_tools, "_install_tool", return_value=True) as mock_install:
|
||||
result = runner.invoke(install_tools.main, [])
|
||||
assert result.exit_code == 0
|
||||
assert mock_install.call_count == 6
|
||||
assert mock_install.call_count == 7
|
||||
|
||||
def test_install_specific_tool(self) -> None:
|
||||
runner = CliRunner()
|
||||
|
||||
@@ -68,10 +68,12 @@ class TestPushToBadgesBranch:
|
||||
|
||||
sha_result = MagicMock()
|
||||
sha_result.stdout = "abc123\n"
|
||||
diff_result = MagicMock()
|
||||
diff_result.stdout = "coverage.svg\n"
|
||||
default_result = MagicMock()
|
||||
with patch(
|
||||
"subprocess.run",
|
||||
side_effect=[default_result] * 7 + [sha_result],
|
||||
side_effect=[default_result] * 6 + [diff_result] + [default_result, default_result, sha_result],
|
||||
) as mock_run:
|
||||
sha = push_badges.push_to_badges_branch(str(badges_dir))
|
||||
|
||||
@@ -143,6 +145,40 @@ class TestUpdateReadmeWithBadgeSha:
|
||||
push_badges.update_readme_with_badge_sha("abc123def456", repo_root=tmp_path)
|
||||
# Should not raise
|
||||
|
||||
def test_version_verification_stale(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
readme = tmp_path / "README.md"
|
||||
readme.write_text("[]")
|
||||
monkeypatch.chdir(tmp_path)
|
||||
badges_dir = tmp_path / ".badges"
|
||||
badges_dir.mkdir(exist_ok=True)
|
||||
(badges_dir / "version.svg").write_text("version: v0.27.0")
|
||||
with patch("subprocess.run"):
|
||||
with patch("devx.tools.generate_badges.detect_package_name", return_value="devx"):
|
||||
with patch("devx.tools.generate_badges.read_version", return_value="0.33.4"):
|
||||
push_badges.update_readme_with_badge_sha("abc123def456", repo_root=tmp_path)
|
||||
|
||||
def test_version_verification_current(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
readme = tmp_path / "README.md"
|
||||
readme.write_text("[]")
|
||||
monkeypatch.chdir(tmp_path)
|
||||
badges_dir = tmp_path / ".badges"
|
||||
badges_dir.mkdir(exist_ok=True)
|
||||
(badges_dir / "version.svg").write_text("version: v0.33.4")
|
||||
with patch("subprocess.run"):
|
||||
with patch("devx.tools.generate_badges.detect_package_name", return_value="devx"):
|
||||
with patch("devx.tools.generate_badges.read_version", return_value="0.33.4"):
|
||||
push_badges.update_readme_with_badge_sha("abc123def456", repo_root=tmp_path)
|
||||
|
||||
def test_version_verification_no_badges_dir(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
readme = tmp_path / "README.md"
|
||||
readme.write_text("[]")
|
||||
monkeypatch.chdir(tmp_path)
|
||||
# No .badges/version.svg exists — should skip verification gracefully
|
||||
with patch("subprocess.run"):
|
||||
with patch("devx.tools.generate_badges.detect_package_name", return_value="devx"):
|
||||
with patch("devx.tools.generate_badges.read_version", return_value="0.33.4"):
|
||||
push_badges.update_readme_with_badge_sha("abc123def456", repo_root=tmp_path)
|
||||
|
||||
|
||||
class TestMain:
|
||||
def test_success(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
|
||||
@@ -27,6 +27,7 @@ from devx.ci.release import (
|
||||
run_tests,
|
||||
tag_exists,
|
||||
update_changelog,
|
||||
update_doc_versions,
|
||||
update_init_version,
|
||||
verify_alignment,
|
||||
verify_tag_consistency,
|
||||
@@ -801,7 +802,7 @@ class TestCommitReleaseChanges:
|
||||
result = commit_release_changes("0.2.0")
|
||||
assert result is True
|
||||
calls = [c.args[0] for c in mock_run_cmd.call_args_list]
|
||||
assert ["git", "add", "src/devx/__init__.py", "CHANGELOG.md"] in calls
|
||||
assert ["git", "add", "src/devx/__init__.py", "CHANGELOG.md", "README.md", "docs/"] in calls
|
||||
assert ["git", "commit", "--no-verify", "-m", "release: v0.2.0 [skip ci]"] in calls
|
||||
|
||||
@patch("devx.ci.release.run_cmd")
|
||||
@@ -814,6 +815,21 @@ class TestCommitReleaseChanges:
|
||||
assert ["git", "commit", "--no-verify", "-m", "release: v0.1.0 [skip ci]"] not in calls
|
||||
|
||||
|
||||
class TestUpdateDocVersions:
|
||||
@patch("subprocess.run")
|
||||
def test_success(self, mock_run: MagicMock) -> None:
|
||||
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
||||
update_doc_versions("0.33.4")
|
||||
assert mock_run.called
|
||||
|
||||
@patch("subprocess.run")
|
||||
def test_failure_warns(self, mock_run: MagicMock) -> None:
|
||||
mock_run.return_value = MagicMock(returncode=1, stdout="", stderr="some error")
|
||||
# Should not raise
|
||||
update_doc_versions("0.33.4")
|
||||
assert mock_run.called
|
||||
|
||||
|
||||
class TestCreateAndPushTag:
|
||||
@patch("devx.ci.release.tag_exists", return_value=False)
|
||||
@patch("devx.ci.release.run_cmd")
|
||||
|
||||
Reference in New Issue
Block a user