| """Helper functions for managing release tracking issues and checklists.""" |
| |
| import re |
| |
| |
| def parse_metadata_line(line): |
| """Parses a checklist line with optional | key=value metadata.""" |
| match = re.match(r"^\s*-\s*\[([ xX])\]\s+([^|]+)(?:\s*\|\s*(.*))?$", line) |
| if not match: |
| return None |
| |
| checked = match.group(1).lower() == "x" |
| name = match.group(2).strip() |
| metadata_str = match.group(3) |
| |
| metadata = {} |
| if metadata_str: |
| pairs = metadata_str.strip().split() |
| for pair in pairs: |
| if "=" in pair: |
| k, v = pair.split("=", 1) |
| metadata[k] = v |
| |
| return { |
| "checked": checked, |
| "name": name, |
| "metadata": metadata, |
| "original_line": line, |
| } |
| |
| |
| def format_metadata_line(checked, name, metadata): |
| """Formats a checklist line with space-separated key=value metadata.""" |
| check_str = "x" if checked else " " |
| if not metadata: |
| return f"- [{check_str}] {name}" |
| |
| metadata_str = " ".join(f"{k}={v}" for k, v in metadata.items()) |
| return f"- [{check_str}] {name} | {metadata_str}" |
| |
| |
| def update_task_in_body(body, task_name, checked, metadata): |
| """Updates a specific task's checked state and metadata in the issue body.""" |
| lines = body.splitlines() |
| updated_lines = [] |
| found = False |
| |
| for line in lines: |
| parsed = parse_metadata_line(line) |
| if parsed and parsed["name"].lower() == task_name.lower(): |
| updated_lines.append(format_metadata_line(checked, task_name, metadata)) |
| found = True |
| else: |
| updated_lines.append(line) |
| |
| if not found: |
| raise ValueError( |
| f"Task '{task_name}' not found in issue body. " |
| f"Expected format: '- [ ] {task_name}' or '- [x] {task_name}' (optionally followed by '| key=value')" |
| ) |
| |
| return "\n".join(updated_lines) |
| |
| |
| def parse_checklist_state(body): |
| """Parses the main checklist tasks and their metadata.""" |
| state = { |
| "prepare_release": { |
| "checked": False, |
| "status": None, |
| "pr": None, |
| "commit": None, |
| }, |
| "create_branch": { |
| "checked": False, |
| "status": None, |
| "branch": None, |
| "commit": None, |
| }, |
| "tag_final": {"checked": False, "status": None, "tag": None, "commit": None}, |
| "rc_tags": {}, # Dynamically mapped: int -> metadata dict |
| } |
| |
| lines = body.splitlines() |
| for line in lines: |
| parsed = parse_metadata_line(line) |
| if not parsed: |
| continue |
| |
| name = parsed["name"].strip() |
| meta = parsed["metadata"] |
| checked = parsed["checked"] |
| name_lower = name.lower() |
| |
| if "prepare release" in name_lower: |
| state["prepare_release"] = { |
| "checked": checked, |
| "status": meta.get("status"), |
| "pr": meta.get("pr"), |
| "commit": meta.get("commit"), |
| } |
| elif "create release branch" in name_lower: |
| state["create_branch"] = { |
| "checked": checked, |
| "status": meta.get("status"), |
| "branch": meta.get("branch"), |
| "commit": meta.get("commit"), |
| } |
| elif "tag final" in name_lower: |
| state["tag_final"] = { |
| "checked": checked, |
| "status": meta.get("status"), |
| "tag": meta.get("tag"), |
| "commit": meta.get("commit"), |
| } |
| else: |
| # Match Tag RC<num> |
| rc_match = re.match(r"Tag RC(\d+)", name, re.IGNORECASE) |
| if rc_match: |
| rc_num = int(rc_match.group(1)) |
| state["rc_tags"][rc_num] = { |
| "checked": checked, |
| "status": meta.get("status"), |
| "tag": meta.get("tag"), |
| "commit": meta.get("commit"), |
| } |
| |
| return state |