161 lines
5.1 KiB
Python
Executable file
161 lines
5.1 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""
|
|
Script Name: compare-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 comparison between trees.
|
|
v1.0.0 - 2026-06-08: Initial release.
|
|
|
|
Example layout::
|
|
|
|
tree-a/charts/monitoring/alpha/Chart.yaml # version: 1.2.0
|
|
tree-b/charts/monitoring/alpha/Chart.yaml # version: 1.3.0
|
|
|
|
Run::
|
|
|
|
./compare-helm-chart-versions.py tree-a tree-b alpha
|
|
|
|
Example output::
|
|
|
|
=== DIFFERENCE: charts/monitoring [alpha] ===
|
|
tree-a 1.2.0
|
|
tree-b 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:
|
|
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 collect_versions(
|
|
root: Path,
|
|
min_depth: int,
|
|
targets: set[str] | None,
|
|
) -> dict[str, dict[str, str]]:
|
|
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) < 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
|
|
if targets and target not in targets:
|
|
continue
|
|
data[group][target] = version
|
|
return data
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(
|
|
description="Compare Helm chart versions between two directory trees.",
|
|
epilog=(
|
|
"example:\n"
|
|
" %(prog)s tree-a tree-b alpha\n"
|
|
" %(prog)s tree-a tree-b --label-a release --label-b main alpha beta\n\n"
|
|
"Compares matching Chart.yaml files grouped by path. With the layout above,\n"
|
|
"reports version 1.2.0 in tree-a vs 1.3.0 in tree-b for target 'alpha'."
|
|
),
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
)
|
|
parser.add_argument("tree_a", type=Path, help="First directory tree")
|
|
parser.add_argument("tree_b", type=Path, help="Second directory tree")
|
|
parser.add_argument(
|
|
"targets",
|
|
nargs="*",
|
|
help="Optional leaf directory names to compare (default: all found)",
|
|
)
|
|
parser.add_argument(
|
|
"--min-depth",
|
|
type=int,
|
|
default=2,
|
|
metavar="N",
|
|
help="Minimum path depth below root for Chart.yaml (default: 2)",
|
|
)
|
|
parser.add_argument(
|
|
"--label-a",
|
|
default=None,
|
|
help="Display label for the first tree (default: directory name)",
|
|
)
|
|
parser.add_argument(
|
|
"--label-b",
|
|
default=None,
|
|
help="Display label for the second tree (default: directory name)",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
for label, path in (("tree_a", args.tree_a), ("tree_b", args.tree_b)):
|
|
if not path.is_dir():
|
|
print(f"Error: {label} is not a directory: {path}", file=sys.stderr)
|
|
return 1
|
|
|
|
tree_a = args.tree_a.resolve()
|
|
tree_b = args.tree_b.resolve()
|
|
label_a = args.label_a or tree_a.name
|
|
label_b = args.label_b or tree_b.name
|
|
target_filter = set(args.targets) if args.targets else None
|
|
|
|
versions_a = collect_versions(tree_a, args.min_depth, target_filter)
|
|
versions_b = collect_versions(tree_b, args.min_depth, target_filter)
|
|
|
|
all_groups = sorted(set(versions_a) | set(versions_b))
|
|
differences_found = False
|
|
|
|
for group in all_groups:
|
|
targets_a = versions_a.get(group, {})
|
|
targets_b = versions_b.get(group, {})
|
|
all_targets = sorted(set(targets_a) | set(targets_b))
|
|
for target in all_targets:
|
|
va = targets_a.get(target)
|
|
vb = targets_b.get(target)
|
|
if va != vb:
|
|
differences_found = True
|
|
print(f"=== DIFFERENCE: {group} [{target}] ===")
|
|
print(f" {label_a:20s} {va or '(missing)'}")
|
|
print(f" {label_b:20s} {vb or '(missing)'}")
|
|
print()
|
|
|
|
if not differences_found:
|
|
print("No chart version differences found for the selected trees.")
|
|
return 1 if differences_found else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|