helm-chart-version-tools/check-helm-chart-versions.py

135 lines
3.9 KiB
Python
Executable file

#!/usr/bin/env python3
"""
Script Name: check-helm-chart-versions.py
Description:
v1.2.0 - 2026-06-09: Added usage example in docstring and --help.
v1.1.0 - 2026-06-09: Generic Helm chart version drift checker.
v1.0.0 - 2026-06-08: Initial release.
Example layout::
charts/
monitoring/
alpha/Chart.yaml # dependencies version: 1.2.0
beta/Chart.yaml # dependencies version: 1.3.0
Run::
./check-helm-chart-versions.py charts
Example output::
=== MISALIGNMENT FOUND: monitoring ===
alpha 1.2.0
beta 1.3.0
"""
from __future__ import annotations
import argparse
import re
import sys
from collections import defaultdict
from pathlib import Path
_DEPENDENCY_RE = re.compile(
r"dependencies:\s*\n(?:[^\n]*\n)*?\s*-\s*name:[^\n]*\n(?:[^\n]*\n)*?\s*version:\s*([^\n]+)",
)
_APP_VERSION_RE = re.compile(r"appVersion:\s*([^\n]+)")
def extract_version(chart_file: Path) -> str | None:
content = chart_file.read_text(encoding="utf-8")
match = _DEPENDENCY_RE.search(content)
if match:
return match.group(1).strip().strip("\"'")
match = _APP_VERSION_RE.search(content)
if match:
return match.group(1).strip().strip("\"'")
return None
def parse_chart_location(chart_file: Path, root: Path) -> tuple[str, str] | None:
"""
Map a Chart.yaml path to (group, target).
group: parent directories relative to root, excluding the leaf directory
target: leaf directory name immediately containing Chart.yaml
"""
try:
rel_parent = chart_file.parent.relative_to(root)
except ValueError:
return None
if not rel_parent.parts:
return None
target = rel_parent.parts[-1]
group = "/".join(rel_parent.parts[:-1]) if len(rel_parent.parts) > 1 else "."
return group, target
def find_chart_files(root: Path) -> list[Path]:
return sorted(root.rglob("Chart.yaml"))
def main() -> int:
parser = argparse.ArgumentParser(
description="Find Helm chart version drift across directory variants.",
epilog=(
"example:\n"
" %(prog)s charts\n"
" %(prog)s charts --min-depth 3\n\n"
"Given charts/monitoring/alpha/Chart.yaml (1.2.0) and\n"
"charts/monitoring/beta/Chart.yaml (1.3.0), reports a mismatch\n"
"under group 'monitoring'."
),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"root",
nargs="?",
default=".",
type=Path,
help="Directory tree to scan (default: current directory)",
)
parser.add_argument(
"--min-depth",
type=int,
default=2,
metavar="N",
help="Minimum path depth below root for Chart.yaml (default: 2)",
)
args = parser.parse_args()
root = args.root.resolve()
if not root.is_dir():
print(f"Error: not a directory: {root}", file=sys.stderr)
return 1
app_data: dict[str, dict[str, str]] = defaultdict(dict)
for chart_file in find_chart_files(root):
if len(chart_file.parent.relative_to(root).parts) < args.min_depth:
continue
location = parse_chart_location(chart_file, root)
version = extract_version(chart_file)
if not location or not version:
continue
group, target = location
app_data[group][target] = version
misalignments_found = False
for group in sorted(app_data):
targets = app_data[group]
if len(set(targets.values())) > 1:
misalignments_found = True
print(f"=== MISALIGNMENT FOUND: {group} ===")
for target in sorted(targets):
print(f" {target:20s} {targets[target]}")
print()
if not misalignments_found:
print("No version misalignments found.")
return 1 if misalignments_found else 0
if __name__ == "__main__":
raise SystemExit(main())