Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ driftcheck --json # machine-readable
driftcheck --fix # auto-fix drifts in documentation files
```

Checks (v0.1.9):
Checks (v0.1.10):
- Maven: `pom.xml` `java.version`, `maven.compiler.source`, `maven.compiler.target`, `release` vs README mentions — major-version comparison
- Docker: `Dockerfile` `FROM <image>:<tag>` vs `README.md` / `docs/README*.md` / `CONTRIBUTING*.md` — handles variant tags (`24` matches `24-slim`, `24-alpine`), multi-stage builds (`FROM golang:1.23 AS builder` → `FROM alpine:3.21`)
- Java/Gradle: `build.gradle` `sourceCompatibility`, `jvmTarget`, `JavaVersion.VERSION_*` vs README mentions
- Rust: `rust-toolchain.toml` `channel` **and** `Cargo.toml` `rust-version` vs `README.md` / `docs/README*.md` / `CONTRIBUTING*.md`
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "driftcheck"
version = "0.1.9"
version = "0.1.10"
description = "Detect version drift between docs and toolchain files (README vs Dockerfile, build.gradle, rust-toolchain.toml, package.json, etc.)"
readme = "README.md"
requires-python = ">=3.10"
Expand Down
2 changes: 1 addition & 1 deletion src/driftcheck/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
"""driftcheck — detect version drift between docs and toolchain."""
__version__ = "0.1.3"
__version__ = "0.1.10"
7 changes: 5 additions & 2 deletions src/driftcheck/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,18 @@ def main(argv=None) -> int:
external_resource_drifts = result.get("external_resource_drifts", [])
docker_drifts = result.get("docker_drifts", [])
java_drifts = result.get("java_drifts", [])
maven_drifts = result.get("maven_drifts", [])
tv = result.get("toolchain_version")
cv = result.get("cargo_rust_version")
nv = result.get("package_node")
pv = result.get("pyproject_python")
gv = result.get("gomod_version")

if not tv and not cv and not nv and not pv and not gv and not count_drifts and not actions_drifts and not lineending_drifts and not external_resource_drifts and not docker_drifts and not java_drifts:
if not tv and not cv and not nv and not pv and not gv and not count_drifts and not actions_drifts and not lineending_drifts and not external_resource_drifts and not docker_drifts and not java_drifts and not maven_drifts:
print("driftcheck: no toolchain version found")
return 0

if not drifts and not rust_drifts and not node_drifts and not python_drifts and not go_drifts and not count_drifts and not actions_drifts and not lineending_drifts and not docker_drifts and not java_drifts:
if not drifts and not rust_drifts and not node_drifts and not python_drifts and not go_drifts and not count_drifts and not actions_drifts and not lineending_drifts and not docker_drifts and not java_drifts and not maven_drifts:
parts = []
if tv: parts.append(f"Rust {tv}")
if cv: parts.append(f"Rust(Cargo) {cv}")
Expand Down Expand Up @@ -86,6 +87,8 @@ def main(argv=None) -> int:
print(f"driftcheck: {d['file']}: {d['doc_image']} → should be {d['dockerfile_image']} (Dockerfile)")
for d in java_drifts:
print(f"driftcheck: {d['file']}: Java {d['doc_version']} → should be {d['gradle_version']} (build.gradle)")
for d in maven_drifts:
print(f"driftcheck: {d['file']}: Java {d['doc_version']} → should be {d['maven_version']} (pom.xml)")
return 1

if __name__ == "__main__":
Expand Down
42 changes: 42 additions & 0 deletions src/driftcheck/detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,39 @@ def find_java_drift(gradle_text: str, docs: dict[str, str]) -> list[dict]:
return drifts


# ---------------------------------------------------------------------------
# Maven drift: pom.xml version vs README
# ---------------------------------------------------------------------------
MAVEN_VER_RE = re.compile(r'<(?:java\.version|maven\.compiler\.source|maven\.compiler\.target|release)>(?P<ver>\d+(?:\.\d+)?)</')
MAVEN_DOC_RE = re.compile(r'(?:Java|JDK|JRE|requires)\s+(?P<ver>\d+(?:\.\d+)?)', re.I)

def parse_maven_java_version(text: str) -> str | None:
"""Parse Java version from pom.xml java.version or maven.compiler.source."""
m = MAVEN_VER_RE.search(text)
return m.group("ver") if m else None

def find_maven_drift(pom_text: str, docs: dict[str, str]) -> list[dict]:
"""Detect drift between pom.xml Java version and README mentions."""
mv = parse_maven_java_version(pom_text)
if not mv:
return []
drifts = []
for fname, content in docs.items():
for m in MAVEN_DOC_RE.finditer(content):
dv = m.group("ver")
dv_major = dv.split(".")[0]
mv_major = mv.split(".")[0]
if dv_major != mv_major:
drifts.append({
"file": fname,
"doc_version": dv,
"maven_version": mv,
"pos": m.start(),
})
break
return drifts


def apply_fixes(root: Path, result: dict) -> list[str]:
"""Apply fixes for all detected drifts. Returns list of fixed file paths."""
fixed = []
Expand Down Expand Up @@ -590,6 +623,13 @@ def scan_repo(root: Path = Path(".")) -> dict:
if p.is_file():
gradle_files[str(p.relative_to(root))] = p.read_text(encoding="utf-8", errors="replace")

# Maven pom.xml files
maven_files = {}
for pattern in ["pom.xml", "maven/pom.xml"]:
for p in root.glob(pattern):
if p.is_file():
maven_files[str(p.relative_to(root))] = p.read_text(encoding="utf-8", errors="replace")

rust_drifts = find_rust_drift(toolchain_text, docs)
rust_drifts_multi = find_rust_drift_multi(toolchain_text, cargo_text, docs)
node_drifts = find_node_drift(package_text, docs)
Expand All @@ -601,6 +641,7 @@ def scan_repo(root: Path = Path(".")) -> dict:
external_resource_drifts = find_external_resource_drift(root)
docker_drifts = find_docker_drift(dockerfiles, docs)
java_drifts = find_java_drift("\n".join(gradle_files.values()), docs)
maven_drifts = find_maven_drift("\n".join(maven_files.values()), docs)

return {
"toolchain_version": parse_toolchain_version(toolchain_text),
Expand All @@ -619,4 +660,5 @@ def scan_repo(root: Path = Path(".")) -> dict:
"external_resource_drifts": external_resource_drifts,
"docker_drifts": docker_drifts,
"java_drifts": java_drifts,
"maven_drifts": maven_drifts,
}
41 changes: 41 additions & 0 deletions tests/test_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,47 @@ def test_java_detects_drift():
def test_java_no_gradle_returns_empty():
assert find_java_drift("", {"README.md": "Java 17"}) == []


# ---- Maven drift tests ----
from driftcheck.detector import find_maven_drift, parse_maven_java_version

def test_parse_maven_java_version():
pom = "<java.version>17</java.version>"
assert parse_maven_java_version(pom) == "17"

def test_parse_maven_compiler_source():
pom = "<maven.compiler.source>21</maven.compiler.source>"
assert parse_maven_java_version(pom) == "21"

def test_parse_maven_no_java_info():
pom = "<project><groupId>com.example</groupId></project>"
assert parse_maven_java_version(pom) is None

def test_maven_no_drift():
pom = "<java.version>17</java.version>"
docs = {"README.md": "Requires Java 17"}
assert find_maven_drift(pom, docs) == []

def test_maven_detects_drift():
pom = "<java.version>17</java.version>"
docs = {"README.md": "Requires Java 11"}
drifts = find_maven_drift(pom, docs)
assert len(drifts) == 1
assert drifts[0]["doc_version"] == "11"
assert drifts[0]["maven_version"] == "17"

def test_maven_no_pom_returns_empty():
assert find_maven_drift("", {"README.md": "Java 17"}) == []

def test_maven_drift_included_in_scan():
with tempfile.TemporaryDirectory() as td:
root = Path(td)
(root / "pom.xml").write_text("<java.version>21</java.version>")
(root / "README.md").write_text("Requires Java 17 to build")
result = scan_repo(root)
assert "maven_drifts" in result
assert len(result["maven_drifts"]) == 1

def test_java_drift_included_in_scan():
with tempfile.TemporaryDirectory() as td:
root = Path(td)
Expand Down
Loading