| """YAML parsing helpers.""" | |
| from typing import Any, Optional, Tuple | |
| import yaml | |
| def safe_parse_yaml(content: str) -> Tuple[Optional[Any], Optional[str]]: | |
| """Parse YAML content safely. | |
| Returns (parsed, error_message). If parsing succeeds, error_message is None. | |
| If parsing fails, parsed is None and error_message contains the description. | |
| """ | |
| try: | |
| parsed = yaml.safe_load(content) | |
| return parsed, None | |
| except yaml.YAMLError as exc: | |
| return None, str(exc) | |
| def is_valid_workflow(content: str) -> Tuple[bool, Optional[str]]: | |
| """Check if content is a valid GitHub Actions workflow YAML. | |
| Returns (is_valid, error_message). | |
| """ | |
| parsed, err = safe_parse_yaml(content) | |
| if err: | |
| return False, f"YAML parse error: {err}" | |
| if not isinstance(parsed, dict): | |
| return False, "Workflow root must be a mapping" | |
| if "jobs" not in parsed: | |
| return False, "Workflow must define 'jobs'" | |
| jobs = parsed.get("jobs") | |
| if not isinstance(jobs, dict) or not jobs: | |
| return False, "Workflow must define at least one job" | |
| for job_name, job in jobs.items(): | |
| if not isinstance(job, dict): | |
| return False, f"Job '{job_name}' must be a mapping" | |
| if "runs-on" not in job: | |
| return False, f"Job '{job_name}' is missing 'runs-on'" | |
| return True, None | |