Skip to content

Commit 502d9ee

Browse files
committed
feat(audit): integrate multi-version detection into update flow
Multi-version runtimes (PHP, Python, Node.js, Ruby, Go) are now fully integrated into the main update flow: - `make update` detects ALL installed versions for multi-version runtimes - Snapshot contains entries like php@8.5, php@8.4, python@3.14, node@25 - Each version cycle has its own status (UP-TO-DATE, OUTDATED, NOT INSTALLED) - Upgrade hints show per-version upgrade paths (e.g., "8.4.16 → 8.4.17") The --versions flag is now deprecated in favor of the integrated flow.
1 parent 009a31e commit 502d9ee

1 file changed

Lines changed: 156 additions & 2 deletions

File tree

audit.py

Lines changed: 156 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,99 @@ def collect_latest_version(tool: Tool, offline_cache: dict[str, tuple[str, str]]
129129
return ("", "")
130130

131131

132+
def audit_multi_version_tool(
133+
tool_name: str,
134+
catalog_data: dict,
135+
mv_config: dict,
136+
) -> list[dict[str, str]]:
137+
"""Audit a multi-version runtime tool.
138+
139+
For tools like PHP, Python, Node.js that support multiple concurrent versions,
140+
this generates separate audit entries for each supported version cycle.
141+
142+
Args:
143+
tool_name: Base tool name (e.g., "php", "python")
144+
catalog_data: Full catalog entry data
145+
mv_config: Multi-version configuration from catalog
146+
147+
Returns:
148+
List of audit result dictionaries, one per version cycle
149+
"""
150+
product = mv_config.get("product", tool_name)
151+
max_versions = mv_config.get("max_versions", 4)
152+
153+
# Fetch supported versions from endoflife.date
154+
try:
155+
supported = collect_endoflife(product, max_versions=max_versions)
156+
except Exception:
157+
return []
158+
159+
if not supported:
160+
return []
161+
162+
# Detect installed versions
163+
detected = detect_multi_versions(tool_name, mv_config, supported)
164+
165+
results = []
166+
for version_info in detected:
167+
cycle = version_info.get("cycle", "")
168+
latest = version_info.get("latest_upstream", "")
169+
installed = version_info.get("installed")
170+
path = version_info.get("path", "")
171+
method = version_info.get("install_method", "")
172+
status_lifecycle = version_info.get("status", "unknown")
173+
174+
# Determine audit status
175+
if installed:
176+
if installed == latest:
177+
status = "UP-TO-DATE"
178+
else:
179+
status = "OUTDATED"
180+
else:
181+
status = "NOT INSTALLED"
182+
183+
# Build versioned tool name
184+
versioned_name = f"{tool_name}@{cycle}"
185+
186+
# Classification reason
187+
if method:
188+
classification_reason = f"Detected via path analysis: {method}"
189+
else:
190+
classification_reason = "No installation detected"
191+
192+
# Hint for not installed or outdated
193+
if status == "NOT INSTALLED":
194+
hint = f"Install {tool_name} {cycle}: check your package manager or version manager"
195+
elif status == "OUTDATED":
196+
hint = f"Upgrade {tool_name} {cycle}: {installed}{latest}"
197+
else:
198+
hint = ""
199+
200+
results.append({
201+
"tool": versioned_name,
202+
"category": catalog_data.get("category", tool_name),
203+
"installed": installed or "",
204+
"installed_method": method,
205+
"installed_version": installed or "",
206+
"installed_path_selected": path,
207+
"classification_reason_selected": classification_reason,
208+
"latest_upstream": latest,
209+
"latest_version": latest,
210+
"upstream_method": "endoflife",
211+
"status": status,
212+
"tool_url": catalog_data.get("homepage", ""),
213+
"latest_url": f"https://endoflife.date/{product}",
214+
"hint": hint,
215+
# Extra fields for multi-version
216+
"is_multi_version": True,
217+
"base_tool": tool_name,
218+
"version_cycle": cycle,
219+
"lifecycle_status": status_lifecycle, # active, security, eol
220+
})
221+
222+
return results
223+
224+
132225
def audit_tool(tool: Tool, offline_cache: dict[str, tuple[str, str]] | None = None) -> dict[str, str]:
133226
"""Audit a single tool.
134227
@@ -405,16 +498,34 @@ def cmd_update(args: argparse.Namespace) -> int:
405498
print(f"# Estimated time: ~{est_time}s (timeout=3s per tool, {MAX_WORKERS} workers)", file=sys.stderr)
406499
print("", file=sys.stderr)
407500

408-
# Group tools by category for organized output
501+
# Identify multi-version tools
502+
from cli_audit.catalog import ToolCatalog
503+
catalog = ToolCatalog()
504+
multi_version_tools = {} # tool_name -> (catalog_data, mv_config)
505+
regular_tools = []
506+
507+
for tool in tools_list:
508+
if catalog.has_tool(tool.name):
509+
catalog_data = catalog.get_raw_data(tool.name)
510+
mv_config = catalog_data.get("multi_version", {})
511+
if mv_config.get("enabled"):
512+
multi_version_tools[tool.name] = (catalog_data, mv_config)
513+
continue
514+
regular_tools.append(tool)
515+
516+
# Group regular tools by category for organized output
409517
from cli_audit.render import CATEGORY_ORDER, CATEGORY_ICON, CATEGORY_DESC
410518
categorized: dict[str, list] = {}
411-
for tool in tools_list:
519+
for tool in regular_tools:
412520
cat = tool.category or "general"
413521
if cat not in categorized:
414522
categorized[cat] = []
415523
categorized[cat].append(tool)
416524
sorted_cats = sorted(categorized.keys(), key=lambda c: CATEGORY_ORDER.get(c, 99))
417525

526+
# Total includes both regular tools and multi-version entries
527+
total = len(regular_tools) + len(multi_version_tools)
528+
418529
# Parallel audit with progress tracking, grouped by category
419530
results = []
420531
completed = 0
@@ -506,6 +617,49 @@ def cmd_update(args: argparse.Namespace) -> int:
506617
"latest_url": "",
507618
"hint": "",
508619
})
620+
621+
# Audit multi-version runtimes
622+
if multi_version_tools:
623+
# ANSI colors
624+
GREEN = "\033[32m"
625+
BOLD_GREEN = "\033[1;32m"
626+
YELLOW = "\033[33m"
627+
BLUE = "\033[34m"
628+
RESET = "\033[0m"
629+
630+
print(f"\n# 🔄 Multi-version runtimes ({len(multi_version_tools)} runtimes)", file=sys.stderr)
631+
632+
for tool_name, (catalog_data, mv_config) in multi_version_tools.items():
633+
completed += 1
634+
print(f"# [{completed}/{total}] {tool_name} (multi-version)...", file=sys.stderr, flush=True)
635+
636+
mv_results = audit_multi_version_tool(tool_name, catalog_data, mv_config)
637+
638+
for mv_result in mv_results:
639+
results.append(mv_result)
640+
641+
# Progress output for each version
642+
versioned_name = mv_result.get("tool", "")
643+
inst = mv_result.get("installed", "")
644+
latest = mv_result.get("latest_upstream", "")
645+
status = mv_result.get("status", "")
646+
647+
if status == "UP-TO-DATE":
648+
inst_color = GREEN
649+
op = "==="
650+
elif status == "OUTDATED":
651+
inst_color = YELLOW
652+
op = "!=="
653+
else:
654+
inst_color = BLUE
655+
op = "?"
656+
657+
inst_display = inst if inst else "n/a"
658+
latest_display = latest if latest else "n/a"
659+
inst_fmt = f"{inst_color}{inst_display}{RESET}"
660+
latest_fmt = f"{BOLD_GREEN}{latest_display}{RESET}"
661+
print(f"# → {versioned_name}: {inst_fmt} {op} {latest_fmt}", file=sys.stderr, flush=True)
662+
509663
except KeyboardInterrupt:
510664
# Shutdown executor immediately without waiting for threads
511665
executor.shutdown(wait=False, cancel_futures=True)

0 commit comments

Comments
 (0)